diff --git a/composer.lock b/composer.lock index 8388f78..71ed5e0 100644 --- a/composer.lock +++ b/composer.lock @@ -8,7 +8,7 @@ "packages": [ { "name": "api-platform/doctrine-common", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-common.git", @@ -89,13 +89,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-common/tree/v4.0.17" + "source": "https://github.com/api-platform/doctrine-common/tree/v4.0.18" }, "time": "2024-09-21T11:01:16+00:00" }, { "name": "api-platform/doctrine-orm", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/doctrine-orm.git", @@ -173,13 +173,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/doctrine-orm/tree/v4.0.17" + "source": "https://github.com/api-platform/doctrine-orm/tree/v4.0.18" }, "time": "2024-12-13T13:34:25+00:00" }, { "name": "api-platform/documentation", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/documentation.git", @@ -234,13 +234,13 @@ ], "description": "API Platform documentation controller.", "support": { - "source": "https://github.com/api-platform/documentation/tree/v4.1.0-beta.1" + "source": "https://github.com/api-platform/documentation/tree/v4.1.0-beta.2" }, "time": "2024-09-21T11:01:16+00:00" }, { "name": "api-platform/http-cache", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/http-cache.git", @@ -311,13 +311,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/http-cache/tree/v4.1.0-beta.1" + "source": "https://github.com/api-platform/http-cache/tree/v4.1.0-beta.2" }, "time": "2024-10-28T09:21:17+00:00" }, { "name": "api-platform/hydra", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/hydra.git", @@ -395,13 +395,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/hydra/tree/v4.0.17" + "source": "https://github.com/api-platform/hydra/tree/v4.0.18" }, "time": "2024-11-06T10:50:57+00:00" }, { "name": "api-platform/json-schema", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/json-schema.git", @@ -479,7 +479,7 @@ }, { "name": "api-platform/jsonld", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/jsonld.git", @@ -550,13 +550,13 @@ "rest" ], "support": { - "source": "https://github.com/api-platform/jsonld/tree/v4.1.0-beta.1" + "source": "https://github.com/api-platform/jsonld/tree/v4.1.0-beta.2" }, "time": "2024-12-19T16:01:35+00:00" }, { "name": "api-platform/metadata", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/metadata.git", @@ -646,22 +646,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/metadata/tree/v4.0.17" + "source": "https://github.com/api-platform/metadata/tree/v4.0.18" }, "time": "2025-02-07T15:44:47+00:00" }, { "name": "api-platform/openapi", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/openapi.git", - "reference": "3145c45ad6e668e20beff2653c1f245296c33d1c" + "reference": "9d96b31ad3eb524a2cb6f89f835deca858beacd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/openapi/zipball/3145c45ad6e668e20beff2653c1f245296c33d1c", - "reference": "3145c45ad6e668e20beff2653c1f245296c33d1c", + "url": "https://api.github.com/repos/api-platform/openapi/zipball/9d96b31ad3eb524a2cb6f89f835deca858beacd0", + "reference": "9d96b31ad3eb524a2cb6f89f835deca858beacd0", "shasum": "" }, "require": { @@ -731,13 +731,13 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/openapi/tree/v4.0.17" + "source": "https://github.com/api-platform/openapi/tree/v4.0.18" }, - "time": "2025-01-09T14:23:11+00:00" + "time": "2025-02-21T09:07:11+00:00" }, { "name": "api-platform/serializer", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/serializer.git", @@ -821,13 +821,13 @@ "serializer" ], "support": { - "source": "https://github.com/api-platform/serializer/tree/v4.0.17" + "source": "https://github.com/api-platform/serializer/tree/v4.1.0-beta.2" }, "time": "2025-02-06T09:32:58+00:00" }, { "name": "api-platform/state", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/state.git", @@ -911,22 +911,22 @@ "swagger" ], "support": { - "source": "https://github.com/api-platform/state/tree/v4.0.17" + "source": "https://github.com/api-platform/state/tree/v4.0.18" }, "time": "2025-01-13T10:20:09+00:00" }, { "name": "api-platform/symfony", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/symfony.git", - "reference": "ea372108b0367ce027d28beef0dbadb454e81ca1" + "reference": "83bae6046fc3aaa7ebc0f78f91ce2e4143b5840d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/symfony/zipball/ea372108b0367ce027d28beef0dbadb454e81ca1", - "reference": "ea372108b0367ce027d28beef0dbadb454e81ca1", + "url": "https://api.github.com/repos/api-platform/symfony/zipball/83bae6046fc3aaa7ebc0f78f91ce2e4143b5840d", + "reference": "83bae6046fc3aaa7ebc0f78f91ce2e4143b5840d", "shasum": "" }, "require": { @@ -1034,13 +1034,13 @@ "symfony" ], "support": { - "source": "https://github.com/api-platform/symfony/tree/v4.0.17" + "source": "https://github.com/api-platform/symfony/tree/v4.0.18" }, - "time": "2025-02-06T09:04:42+00:00" + "time": "2025-02-14T09:53:32+00:00" }, { "name": "api-platform/validator", - "version": "v4.0.17", + "version": "v4.0.18", "source": { "type": "git", "url": "https://github.com/api-platform/validator.git", @@ -2872,25 +2872,24 @@ }, { "name": "gedmo/doctrine-extensions", - "version": "v3.17.1", + "version": "v3.19.0", "source": { "type": "git", "url": "https://github.com/doctrine-extensions/DoctrineExtensions.git", - "reference": "eabb45018c5a4362b46c5beae3881261da89f900" + "reference": "5b0b8a442d19e6701ae64535dc08f7944e2895d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/eabb45018c5a4362b46c5beae3881261da89f900", - "reference": "eabb45018c5a4362b46c5beae3881261da89f900", + "url": "https://api.github.com/repos/doctrine-extensions/DoctrineExtensions/zipball/5b0b8a442d19e6701ae64535dc08f7944e2895d2", + "reference": "5b0b8a442d19e6701ae64535dc08f7944e2895d2", "shasum": "" }, "require": { "behat/transliterator": "^1.2", "doctrine/collections": "^1.2 || ^2.0", - "doctrine/common": "^2.13 || ^3.0", "doctrine/deprecations": "^1.0", "doctrine/event-manager": "^1.2 || ^2.0", - "doctrine/persistence": "^2.2 || ^3.0", + "doctrine/persistence": "^2.2 || ^3.0 || ^4.0", "php": "^7.4 || ^8.0", "psr/cache": "^1 || ^2 || ^3", "psr/clock": "^1", @@ -2898,24 +2897,26 @@ }, "conflict": { "doctrine/annotations": "<1.13 || >=3.0", + "doctrine/common": "<2.13 || >=4.0", "doctrine/dbal": "<3.7 || >=5.0", "doctrine/mongodb-odm": "<2.3 || >=3.0", - "doctrine/orm": "<2.14.0 || 2.16.0 || 2.16.1 || >=4.0" + "doctrine/orm": "<2.20 || >=3.0 <3.3 || >=4.0" }, "require-dev": { "doctrine/annotations": "^1.13 || ^2.0", "doctrine/cache": "^1.11 || ^2.0", + "doctrine/common": "^2.13 || ^3.0", "doctrine/dbal": "^3.7 || ^4.0", "doctrine/doctrine-bundle": "^2.3", "doctrine/mongodb-odm": "^2.3", - "doctrine/orm": "^2.14.0 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.14.0", + "doctrine/orm": "^2.20 || ^3.3", + "friendsofphp/php-cs-fixer": "^3.70", "nesbot/carbon": "^2.71 || ^3.0", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-doctrine": "^1.4", - "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan": "^2.1.1", + "phpstan/phpstan-doctrine": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.3", "phpunit/phpunit": "^9.6", - "rector/rector": "^1.1", + "rector/rector": "^2.0.6", "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/doctrine-bridge": "^5.4 || ^6.0 || ^7.0", "symfony/phpunit-bridge": "^6.0 || ^7.0", @@ -2977,7 +2978,7 @@ "support": { "email": "gediminas.morkevicius@gmail.com", "issues": "https://github.com/doctrine-extensions/DoctrineExtensions/issues", - "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.17.1", + "source": "https://github.com/doctrine-extensions/DoctrineExtensions/tree/v3.19.0", "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc" }, "funding": [ @@ -2998,20 +2999,20 @@ "type": "github" } ], - "time": "2024-10-07T22:30:27+00:00" + "time": "2025-02-24T22:12:57+00:00" }, { "name": "goteo/benzina", - "version": "0.3", + "version": "0.3.1", "source": { "type": "git", - "url": "https://github.com/GoteoFoundation/benzina.git", - "reference": "83cc705b963868dc481f6dd7c6d699b72b537586" + "url": "https://github.com/goteo/benzina.git", + "reference": "c19f8671285f1050a11582f8dd17f87888cb727c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GoteoFoundation/benzina/zipball/83cc705b963868dc481f6dd7c6d699b72b537586", - "reference": "83cc705b963868dc481f6dd7c6d699b72b537586", + "url": "https://api.github.com/repos/goteo/benzina/zipball/c19f8671285f1050a11582f8dd17f87888cb727c", + "reference": "c19f8671285f1050a11582f8dd17f87888cb727c", "shasum": "" }, "require": { @@ -3054,10 +3055,10 @@ ], "description": "Migration tool to pump v3 data into a v4 schema", "support": { - "issues": "https://github.com/GoteoFoundation/benzina/issues", - "source": "https://github.com/GoteoFoundation/benzina/tree/0.3" + "issues": "https://github.com/goteo/benzina/issues", + "source": "https://github.com/goteo/benzina/tree/0.3.1" }, - "time": "2025-02-13T20:29:02+00:00" + "time": "2025-02-21T15:13:32+00:00" }, { "name": "jolicode/automapper", @@ -4559,16 +4560,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "70d60e9a3603108563010f8592dff15a6f15dfae" + "reference": "b209751ed25f735ea90ca4c9c969d9413a17dfee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/70d60e9a3603108563010f8592dff15a6f15dfae", - "reference": "70d60e9a3603108563010f8592dff15a6f15dfae", + "url": "https://api.github.com/repos/symfony/cache/zipball/b209751ed25f735ea90ca4c9c969d9413a17dfee", + "reference": "b209751ed25f735ea90ca4c9c969d9413a17dfee", "shasum": "" }, "require": { @@ -4635,7 +4636,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.16" + "source": "https://github.com/symfony/cache/tree/v6.4.18" }, "funding": [ { @@ -4651,7 +4652,7 @@ "type": "tidelift" } ], - "time": "2024-11-20T10:10:54+00:00" + "time": "2025-01-22T14:13:52+00:00" }, { "name": "symfony/cache-contracts", @@ -5122,21 +5123,21 @@ }, { "name": "symfony/doctrine-bridge", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/doctrine-bridge.git", - "reference": "2ba7e747a944b69f9f583c35173560afebbff995" + "reference": "fd0094d4648bf6bbdafcd1f0c8aafb6e93d735e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/2ba7e747a944b69f9f583c35173560afebbff995", - "reference": "2ba7e747a944b69f9f583c35173560afebbff995", + "url": "https://api.github.com/repos/symfony/doctrine-bridge/zipball/fd0094d4648bf6bbdafcd1f0c8aafb6e93d735e6", + "reference": "fd0094d4648bf6bbdafcd1f0c8aafb6e93d735e6", "shasum": "" }, "require": { "doctrine/event-manager": "^1.2|^2", - "doctrine/persistence": "^3.1", + "doctrine/persistence": "^2.5|^3.1|^4", "php": ">=8.1", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "~1.8", @@ -5210,7 +5211,7 @@ "description": "Provides integration for Doctrine with various Symfony components", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.17" + "source": "https://github.com/symfony/doctrine-bridge/tree/v6.4.18" }, "funding": [ { @@ -5226,7 +5227,7 @@ "type": "tidelift" } ], - "time": "2024-12-18T10:42:42+00:00" + "time": "2025-01-25T08:04:58+00:00" }, { "name": "symfony/dotenv", @@ -5304,16 +5305,16 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "37ad2380e8c1a8cf62a1200a5c10080b679b446c" + "reference": "e8d3b5b1975e67812a54388b1ba8e9ec28eb770e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/37ad2380e8c1a8cf62a1200a5c10080b679b446c", - "reference": "37ad2380e8c1a8cf62a1200a5c10080b679b446c", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/e8d3b5b1975e67812a54388b1ba8e9ec28eb770e", + "reference": "e8d3b5b1975e67812a54388b1ba8e9ec28eb770e", "shasum": "" }, "require": { @@ -5359,7 +5360,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.17" + "source": "https://github.com/symfony/error-handler/tree/v6.4.18" }, "funding": [ { @@ -5375,7 +5376,7 @@ "type": "tidelift" } ], - "time": "2024-12-06T13:30:51+00:00" + "time": "2025-01-06T09:38:16+00:00" }, { "name": "symfony/event-dispatcher", @@ -5797,16 +5798,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "17d8ae2e7aa77154f942e8ac48849ac718b0963f" + "reference": "91df8ee37543ebc01756c9e5eaf94d1878ff1ccd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/17d8ae2e7aa77154f942e8ac48849ac718b0963f", - "reference": "17d8ae2e7aa77154f942e8ac48849ac718b0963f", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/91df8ee37543ebc01756c9e5eaf94d1878ff1ccd", + "reference": "91df8ee37543ebc01756c9e5eaf94d1878ff1ccd", "shasum": "" }, "require": { @@ -5926,7 +5927,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.4.17" + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.18" }, "funding": [ { @@ -5942,20 +5943,20 @@ "type": "tidelift" } ], - "time": "2024-12-19T14:08:41+00:00" + "time": "2025-01-28T18:47:02+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "88898d842eb29d7e1a903724c94e90a6ca9c0509" + "reference": "394b440934056b8d9d6ba250001458e9d7998b7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/88898d842eb29d7e1a903724c94e90a6ca9c0509", - "reference": "88898d842eb29d7e1a903724c94e90a6ca9c0509", + "url": "https://api.github.com/repos/symfony/http-client/zipball/394b440934056b8d9d6ba250001458e9d7998b7f", + "reference": "394b440934056b8d9d6ba250001458e9d7998b7f", "shasum": "" }, "require": { @@ -6019,7 +6020,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.17" + "source": "https://github.com/symfony/http-client/tree/v6.4.18" }, "funding": [ { @@ -6035,7 +6036,7 @@ "type": "tidelift" } ], - "time": "2024-12-18T12:18:31+00:00" + "time": "2025-01-28T15:49:13+00:00" }, { "name": "symfony/http-client-contracts", @@ -6117,16 +6118,16 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "431771b7a6f662f1575b3cfc8fd7617aa9864d57" + "reference": "d0492d6217e5ab48f51fca76f64cf8e78919d0db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/431771b7a6f662f1575b3cfc8fd7617aa9864d57", - "reference": "431771b7a6f662f1575b3cfc8fd7617aa9864d57", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d0492d6217e5ab48f51fca76f64cf8e78919d0db", + "reference": "d0492d6217e5ab48f51fca76f64cf8e78919d0db", "shasum": "" }, "require": { @@ -6174,7 +6175,7 @@ "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.16" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.18" }, "funding": [ { @@ -6190,20 +6191,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T18:58:10+00:00" + "time": "2025-01-09T15:48:56+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "c5647393c5ce11833d13e4b70fff4b571d4ac710" + "reference": "fca7197bfe9e99dfae7fb1ad3f7f5bd9ef80e1b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c5647393c5ce11833d13e4b70fff4b571d4ac710", - "reference": "c5647393c5ce11833d13e4b70fff4b571d4ac710", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/fca7197bfe9e99dfae7fb1ad3f7f5bd9ef80e1b7", + "reference": "fca7197bfe9e99dfae7fb1ad3f7f5bd9ef80e1b7", "shasum": "" }, "require": { @@ -6288,7 +6289,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.17" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.18" }, "funding": [ { @@ -6304,7 +6305,7 @@ "type": "tidelift" } ], - "time": "2024-12-31T14:49:31+00:00" + "time": "2025-01-29T07:25:58+00:00" }, { "name": "symfony/intl", @@ -7096,16 +7097,16 @@ }, { "name": "symfony/property-access", - "version": "v6.4.13", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "8cc779d88d12e440adaa26387bcfc25744064afe" + "reference": "80e0378f2f058b60d87dedc3c760caec882e992c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/8cc779d88d12e440adaa26387bcfc25744064afe", - "reference": "8cc779d88d12e440adaa26387bcfc25744064afe", + "url": "https://api.github.com/repos/symfony/property-access/zipball/80e0378f2f058b60d87dedc3c760caec882e992c", + "reference": "80e0378f2f058b60d87dedc3c760caec882e992c", "shasum": "" }, "require": { @@ -7153,7 +7154,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v6.4.13" + "source": "https://github.com/symfony/property-access/tree/v6.4.18" }, "funding": [ { @@ -7169,20 +7170,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2024-12-16T14:42:05+00:00" }, { "name": "symfony/property-info", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "38b125d78e67668159f75383a293ec0c5d3f2963" + "reference": "94d18e5cc11a37fd92856d38b61d9cdf72536a1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/38b125d78e67668159f75383a293ec0c5d3f2963", - "reference": "38b125d78e67668159f75383a293ec0c5d3f2963", + "url": "https://api.github.com/repos/symfony/property-info/zipball/94d18e5cc11a37fd92856d38b61d9cdf72536a1e", + "reference": "94d18e5cc11a37fd92856d38b61d9cdf72536a1e", "shasum": "" }, "require": { @@ -7193,7 +7194,9 @@ "doctrine/annotations": "<1.12", "phpdocumentor/reflection-docblock": "<5.2", "phpdocumentor/type-resolver": "<1.5.1", - "symfony/dependency-injection": "<5.4|>=6.0,<6.4" + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4|>=6.0,<6.4", + "symfony/serializer": "<5.4" }, "require-dev": { "doctrine/annotations": "^1.12|^2", @@ -7237,7 +7240,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v6.4.17" + "source": "https://github.com/symfony/property-info/tree/v6.4.18" }, "funding": [ { @@ -7253,20 +7256,20 @@ "type": "tidelift" } ], - "time": "2024-12-26T19:01:29+00:00" + "time": "2025-01-21T10:52:27+00:00" }, { "name": "symfony/routing", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "91e02e606b4b705c2f4fb42f7e7708b7923a3220" + "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/91e02e606b4b705c2f4fb42f7e7708b7923a3220", - "reference": "91e02e606b4b705c2f4fb42f7e7708b7923a3220", + "url": "https://api.github.com/repos/symfony/routing/zipball/e9bfc94953019089acdfb9be51c1b9142c4afa68", + "reference": "e9bfc94953019089acdfb9be51c1b9142c4afa68", "shasum": "" }, "require": { @@ -7320,7 +7323,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.16" + "source": "https://github.com/symfony/routing/tree/v6.4.18" }, "funding": [ { @@ -7336,7 +7339,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T15:31:34+00:00" + "time": "2025-01-09T08:51:02+00:00" }, { "name": "symfony/runtime", @@ -7531,16 +7534,16 @@ }, { "name": "symfony/security-core", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/security-core.git", - "reference": "19cdb7de86e556202ab16e0cffd1a97348231bc0" + "reference": "0ae7ae716968e00287ab9b7768405e0dc9cad109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-core/zipball/19cdb7de86e556202ab16e0cffd1a97348231bc0", - "reference": "19cdb7de86e556202ab16e0cffd1a97348231bc0", + "url": "https://api.github.com/repos/symfony/security-core/zipball/0ae7ae716968e00287ab9b7768405e0dc9cad109", + "reference": "0ae7ae716968e00287ab9b7768405e0dc9cad109", "shasum": "" }, "require": { @@ -7597,7 +7600,7 @@ "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-core/tree/v6.4.16" + "source": "https://github.com/symfony/security-core/tree/v6.4.18" }, "funding": [ { @@ -7613,7 +7616,7 @@ "type": "tidelift" } ], - "time": "2024-11-27T09:48:51+00:00" + "time": "2025-01-22T20:59:03+00:00" }, { "name": "symfony/security-csrf", @@ -7685,16 +7688,16 @@ }, { "name": "symfony/security-http", - "version": "v6.4.15", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/security-http.git", - "reference": "ded1e078f952e686b058d9eac98e497bea47b308" + "reference": "54f2ccce1f3822eee3fb3a85debd9a67d12762b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-http/zipball/ded1e078f952e686b058d9eac98e497bea47b308", - "reference": "ded1e078f952e686b058d9eac98e497bea47b308", + "url": "https://api.github.com/repos/symfony/security-http/zipball/54f2ccce1f3822eee3fb3a85debd9a67d12762b8", + "reference": "54f2ccce1f3822eee3fb3a85debd9a67d12762b8", "shasum": "" }, "require": { @@ -7753,7 +7756,7 @@ "description": "Symfony Security Component - HTTP Integration", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-http/tree/v6.4.15" + "source": "https://github.com/symfony/security-http/tree/v6.4.18" }, "funding": [ { @@ -7769,20 +7772,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:40:18+00:00" + "time": "2025-01-28T14:53:52+00:00" }, { "name": "symfony/serializer", - "version": "v6.4.15", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "9d862d66198f3c2e30404228629ef4c18d5d608e" + "reference": "6ad986f62276da4c8c69754decfaa445a89cb6e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/9d862d66198f3c2e30404228629ef4c18d5d608e", - "reference": "9d862d66198f3c2e30404228629ef4c18d5d608e", + "url": "https://api.github.com/repos/symfony/serializer/zipball/6ad986f62276da4c8c69754decfaa445a89cb6e3", + "reference": "6ad986f62276da4c8c69754decfaa445a89cb6e3", "shasum": "" }, "require": { @@ -7851,7 +7854,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.4.15" + "source": "https://github.com/symfony/serializer/tree/v6.4.18" }, "funding": [ { @@ -7867,7 +7870,7 @@ "type": "tidelift" } ], - "time": "2024-10-23T13:25:59+00:00" + "time": "2025-01-28T18:47:02+00:00" }, { "name": "symfony/service-contracts", @@ -8522,16 +8525,16 @@ }, { "name": "symfony/validator", - "version": "v6.4.17", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "a3c19a0e542d427c207e22242043ef35b5b99a2c" + "reference": "ce20367d07b2592202e9c266b16a93fa50145207" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/a3c19a0e542d427c207e22242043ef35b5b99a2c", - "reference": "a3c19a0e542d427c207e22242043ef35b5b99a2c", + "url": "https://api.github.com/repos/symfony/validator/zipball/ce20367d07b2592202e9c266b16a93fa50145207", + "reference": "ce20367d07b2592202e9c266b16a93fa50145207", "shasum": "" }, "require": { @@ -8599,7 +8602,7 @@ "description": "Provides tools to validate values", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/validator/tree/v6.4.17" + "source": "https://github.com/symfony/validator/tree/v6.4.18" }, "funding": [ { @@ -8615,20 +8618,20 @@ "type": "tidelift" } ], - "time": "2024-12-29T12:50:19+00:00" + "time": "2025-01-27T16:05:44+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.15", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "38254d5a5ac2e61f2b52f9caf54e7aa3c9d36b80" + "reference": "4ad10cf8b020e77ba665305bb7804389884b4837" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/38254d5a5ac2e61f2b52f9caf54e7aa3c9d36b80", - "reference": "38254d5a5ac2e61f2b52f9caf54e7aa3c9d36b80", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4ad10cf8b020e77ba665305bb7804389884b4837", + "reference": "4ad10cf8b020e77ba665305bb7804389884b4837", "shasum": "" }, "require": { @@ -8684,7 +8687,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.15" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.18" }, "funding": [ { @@ -8700,7 +8703,7 @@ "type": "tidelift" } ], - "time": "2024-11-08T15:28:48+00:00" + "time": "2025-01-17T11:26:11+00:00" }, { "name": "symfony/var-exporter", @@ -8864,16 +8867,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.13", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9" + "reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e99b4e94d124b29ee4cf3140e1b537d2dad8cec9", - "reference": "e99b4e94d124b29ee4cf3140e1b537d2dad8cec9", + "url": "https://api.github.com/repos/symfony/yaml/zipball/bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5", + "reference": "bf598c9d9bb4a22f495a4e26e4c4fce2f8ecefc5", "shasum": "" }, "require": { @@ -8916,7 +8919,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.13" + "source": "https://github.com/symfony/yaml/tree/v6.4.18" }, "funding": [ { @@ -8932,7 +8935,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2025-01-07T09:44:41+00:00" }, { "name": "twig/twig", @@ -9511,16 +9514,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.69.1", + "version": "v3.70.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "13b0c0eede38c11cd674b080f2b485d0f14ffa9f" + "reference": "2ecd5aae0edc937f0d5aa4a22d1d705c6b2e084e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/13b0c0eede38c11cd674b080f2b485d0f14ffa9f", - "reference": "13b0c0eede38c11cd674b080f2b485d0f14ffa9f", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2ecd5aae0edc937f0d5aa4a22d1d705c6b2e084e", + "reference": "2ecd5aae0edc937f0d5aa4a22d1d705c6b2e084e", "shasum": "" }, "require": { @@ -9602,7 +9605,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.69.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.70.0" }, "funding": [ { @@ -9610,7 +9613,7 @@ "type": "github" } ], - "time": "2025-02-18T23:57:43+00:00" + "time": "2025-02-22T23:30:51+00:00" }, { "name": "masterminds/html5", @@ -11903,16 +11906,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v6.4.16", + "version": "v6.4.18", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "4304e6ad5c894a9c72831ad459f627bfd35d766d" + "reference": "fd07959d3e8992795029bdab3605c2e8e895034e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4304e6ad5c894a9c72831ad459f627bfd35d766d", - "reference": "4304e6ad5c894a9c72831ad459f627bfd35d766d", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/fd07959d3e8992795029bdab3605c2e8e895034e", + "reference": "fd07959d3e8992795029bdab3605c2e8e895034e", "shasum": "" }, "require": { @@ -11950,7 +11953,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v6.4.16" + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.18" }, "funding": [ { @@ -11966,7 +11969,7 @@ "type": "tidelift" } ], - "time": "2024-11-13T15:06:22+00:00" + "time": "2025-01-09T15:35:00+00:00" }, { "name": "symfony/maker-bundle", @@ -12381,16 +12384,16 @@ }, { "name": "zenstruck/foundry", - "version": "v2.3.4", + "version": "v2.3.5", "source": { "type": "git", "url": "https://github.com/zenstruck/foundry.git", - "reference": "19d4f79767fa6505ad4092cdb9542de0a747cd85" + "reference": "acd5dfc270bb77d48d62090d27a63d085fddf813" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zenstruck/foundry/zipball/19d4f79767fa6505ad4092cdb9542de0a747cd85", - "reference": "19d4f79767fa6505ad4092cdb9542de0a747cd85", + "url": "https://api.github.com/repos/zenstruck/foundry/zipball/acd5dfc270bb77d48d62090d27a63d085fddf813", + "reference": "acd5dfc270bb77d48d62090d27a63d085fddf813", "shasum": "" }, "require": { @@ -12474,7 +12477,7 @@ ], "support": { "issues": "https://github.com/zenstruck/foundry/issues", - "source": "https://github.com/zenstruck/foundry/tree/v2.3.4" + "source": "https://github.com/zenstruck/foundry/tree/v2.3.5" }, "funding": [ { @@ -12482,7 +12485,7 @@ "type": "github" } ], - "time": "2025-02-14T11:35:54+00:00" + "time": "2025-02-24T13:07:31+00:00" } ], "aliases": [], diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 1c6454f..191ba3a 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -13,7 +13,7 @@ security: app_user_provider: entity: class: App\Entity\User\User - property: username + property: handle firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ diff --git a/src/ApiResource/User/OrganizationApiResource.php b/src/ApiResource/User/OrganizationApiResource.php new file mode 100644 index 0000000..2b9e95c --- /dev/null +++ b/src/ApiResource/User/OrganizationApiResource.php @@ -0,0 +1,73 @@ + new API\Link( + fromClass: UserApiResource::class, + fromProperty: 'organization', + description: 'User identifier' + ), + ] +)] +#[API\Get()] +#[API\Patch( + securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', +)] +class OrganizationApiResource +{ + #[API\ApiProperty(identifier: true, writable: false)] + public UserApiResource $user; + + /** + * ID for tax purposes. e.g: NIF (formerly CIF), Umsatzsteuer-Id, EID, etc. + */ + #[API\ApiProperty( + securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', + )] + #[Assert\NotBlank()] + public string $taxId; + + /** + * Organization legal name before government, + * as it appears on legal documents issued by or for this organization.\ + * Will be used as last option for the display name of the User. + */ + #[API\ApiProperty( + securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', + )] + #[Assert\NotBlank()] + public string $legalName; + + /** + * The name under which the organization presents itself.\ + * Might be similar to the legal name or completely different.\ + * Will be used as display name for the User when present. + */ + #[API\ApiProperty( + securityPostDenormalize: 'is_granted("ORGANIZATION_EDIT", previous_object)', + )] + public string $businessName; +} diff --git a/src/ApiResource/User/PersonApiResource.php b/src/ApiResource/User/PersonApiResource.php new file mode 100644 index 0000000..4ebdddf --- /dev/null +++ b/src/ApiResource/User/PersonApiResource.php @@ -0,0 +1,68 @@ + new API\Link( + fromClass: UserApiResource::class, + fromProperty: 'person', + description: 'User identifier' + ), + ] +)] +#[API\Get()] +#[API\Patch( + securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', +)] +class PersonApiResource +{ + #[API\ApiProperty(identifier: true, writable: false)] + public UserApiResource $user; + + /** + * Personal ID for tax purposes. e.g: NIF, Steuer-ID, SSN, ITIN, etc. + */ + #[API\ApiProperty( + securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', + )] + public string $taxId; + + /** + * First-part of the name of the person, + * in most western conventions this is the given name(s). e.g: John, Juan, etc. + */ + #[API\ApiProperty( + securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', + )] + public string $firstName; + + /** + * Last-part of the name of the person, + * in most western conventions this is the family name(s). e.g: Smith, Herrera García, etc. + */ + #[API\ApiProperty( + securityPostDenormalize: 'is_granted("PERSON_EDIT", previous_object)', + )] + public string $lastName; +} diff --git a/src/ApiResource/User/UserApiResource.php b/src/ApiResource/User/UserApiResource.php index 9365cb8..4833f00 100644 --- a/src/ApiResource/User/UserApiResource.php +++ b/src/ApiResource/User/UserApiResource.php @@ -2,17 +2,19 @@ namespace App\ApiResource\User; -use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; use App\Dto\UserSignupDto; use App\Entity\User\User; +use App\Entity\User\UserType; use App\Filter\OrderedLikeFilter; use App\Filter\UserQueryFilter; -use App\State\ApiResourceStateProcessor; +use App\Mapping\Transformer\UserDisplayNameMapTransformer; use App\State\ApiResourceStateProvider; use App\State\User\UserSignupProcessor; +use App\State\User\UserStateProcessor; +use AutoMapper\Attribute\MapFrom; use Symfony\Component\Validator\Constraints as Assert; /** @@ -22,13 +24,13 @@ use Symfony\Component\Validator\Constraints as Assert; shortName: 'User', stateOptions: new Options(entityClass: User::class), provider: ApiResourceStateProvider::class, - processor: ApiResourceStateProcessor::class, + processor: UserStateProcessor::class, )] #[API\GetCollection()] #[API\Post(input: UserSignupDto::class, processor: UserSignupProcessor::class)] #[API\Get()] -#[API\Patch(securityPostDenormalize: 'is_granted("USER_EDIT", object)')] -#[API\Delete(securityPostDenormalize: 'is_granted("USER_EDIT", object)')] +#[API\Patch(securityPostDenormalize: 'is_granted("USER_EDIT", previous_object)')] +#[API\Delete(securityPostDenormalize: 'is_granted("USER_EDIT", previous_object)')] #[API\ApiFilter(filterClass: UserQueryFilter::class, properties: ['query'])] class UserApiResource { @@ -39,9 +41,6 @@ class UserApiResource #[Assert\Email()] public string $email; - #[API\ApiProperty(writable: false)] - public bool $emailConfirmed; - /** * A unique, non white space, byte-safe string identifier for this User. */ @@ -49,22 +48,48 @@ class UserApiResource #[Assert\NotBlank()] #[Assert\Length(min: 4, max: 30)] #[Assert\Regex('/^[a-z0-9_]+$/')] - public string $username; + public string $handle; /** - * Display name chosen by the User. + * URL to the avatar image of this User. */ - #[API\ApiFilter(filterClass: SearchFilter::class, strategy: 'partial')] - public string $name; + #[Assert\Url()] + public string $avatar; /** - * A list of the roles assigned to this User. Admin scopped property. + * Is this User for an individual acting on their own or a group of individuals? + */ + #[API\ApiProperty(securityPostDenormalize: 'is_granted("USER_EDIT", previous_object)')] + public UserType $type; + + /** + * A list of the roles assigned to this User. Admin scoped property. * * @var array */ - #[API\ApiProperty(securityPostDenormalize: 'is_granted("ROLE_ADMIN")')] + #[API\ApiProperty( + security: 'is_granted("ROLE_ADMIN")', + securityPostDenormalize: 'is_granted("ROLE_ADMIN")' + )] public array $roles; + #[API\ApiProperty(writable: false)] + #[MapFrom(User::class, transformer: UserDisplayNameMapTransformer::class)] + public string $displayName; + + /** + * For `individual` User types: personal data about the User themselves.\ + * For `organization` User types: data for the organization representative or person managing the User. + */ + #[API\ApiProperty(writable: false)] + public PersonApiResource $person; + + /** + * For `organization` User types only. Legal entity data. + */ + #[API\ApiProperty(writable: false)] + public ?OrganizationApiResource $organization = null; + /** * The Accounting for this User monetary movements. */ @@ -78,4 +103,16 @@ class UserApiResource */ #[API\ApiProperty(writable: false)] public array $projects; + + /** + * Has this User confirmed their email address? + */ + #[API\ApiProperty(writable: false)] + public bool $emailConfirmed; + + /** + * A flag determined by the platform for Users who are known to be active. + */ + #[API\ApiProperty(writable: false)] + public bool $active; } diff --git a/src/Benzina/UsersPump.php b/src/Benzina/UsersPump.php index 3db20c7..c786a8c 100644 --- a/src/Benzina/UsersPump.php +++ b/src/Benzina/UsersPump.php @@ -3,6 +3,7 @@ namespace App\Benzina; use App\Entity\User\User; +use App\Service\UserService; use Goteo\Benzina\Pump\AbstractPump; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; @@ -27,11 +28,10 @@ class UsersPump extends AbstractPump public function pump(mixed $record): void { $user = new User(); - $user->setUsername($this->getUsername($record)); + $user->setHandle($this->buildHandle($record)); $user->setPassword($record['password'] ?? ''); $user->setEmail($record['email']); $user->setEmailConfirmed(false); - $user->setName($record['name']); $user->setActive(false); $user->setMigrated(true); $user->setMigratedId($record['id']); @@ -42,40 +42,17 @@ class UsersPump extends AbstractPump ++$this->userCount; } - private function normalizeUsername(string $username): ?string + private function buildHandle(array $record): string { - // If email remove provider - if (\str_contains($username, '@') && \preg_match('/^[\w]+[^@]/', $username, $matches)) { - $username = $matches[0]; - } - - // Only lowercase a-z, numbers and underscore in usernames - $username = \preg_replace('/[^a-z0-9_]/', '_', \strtolower($username)); - - // Min length 4 - $username = \str_pad($username, 4, '_'); - - // Max length 30 - $username = \substr($username, 0, 30); - - if (strlen(str_replace('_', '', $username)) < 1) { - return null; - } - - return $username; - } - - private function getUsername(array $record): string - { - $username = $this->normalizeUsername($record['id']); - - if ($username === null) { - $username = $this->normalizeUsername($record['email']); + try { + $handle = UserService::asHandle($record['id']); + } catch (\Exception $e) { + $handle = UserService::asHandle($record['email']); } $sequenceNumber = \substr($this->userCount, -2, 2); - return \sprintf('%s_%s', $username, $sequenceNumber); + return \sprintf('%s_%s', $handle, $sequenceNumber); } private function getDateCreated(array $record): \DateTime diff --git a/src/Dto/UserSignupDto.php b/src/Dto/UserSignupDto.php index 86146d7..7a5b56b 100644 --- a/src/Dto/UserSignupDto.php +++ b/src/Dto/UserSignupDto.php @@ -2,28 +2,29 @@ namespace App\Dto; +use App\Entity\User\UserType; use Symfony\Component\Validator\Constraints as Assert; final class UserSignupDto { + /** + * A valid e-mail address for the new User. + */ #[Assert\NotBlank()] #[Assert\Email()] public string $email; + /** + * The auth password for the new User. Plaintext string, + * will be hashed by the API. + */ #[Assert\NotBlank()] #[Assert\Length(min: 8)] public string $password; /** - * A unique, non white space, byte-safe string identifier for this User. + * Is this User for an individual acting on their own or a group of individuals? */ #[Assert\NotBlank()] - #[Assert\Length(min: 4, max: 30)] - #[Assert\Regex('/^[a-z0-9_]+$/')] - public string $username; - - /** - * Display name chosen by the User. - */ - public string $name; + public UserType $type = UserType::Individual; } diff --git a/src/Dto/UserTokenLoginDto.php b/src/Dto/UserTokenLoginDto.php index 5befd58..4264cb4 100644 --- a/src/Dto/UserTokenLoginDto.php +++ b/src/Dto/UserTokenLoginDto.php @@ -13,7 +13,7 @@ use Symfony\Component\Validator\Constraints as Assert; final class UserTokenLoginDto { /** - * The identifier (email, username) of the User to be authenticated. + * The identifier (email, handle) of the User to be authenticated. */ #[Assert\NotBlank()] public readonly string $identifier; diff --git a/src/Entity/User/Organization.php b/src/Entity/User/Organization.php new file mode 100644 index 0000000..c28e88d --- /dev/null +++ b/src/Entity/User/Organization.php @@ -0,0 +1,95 @@ +setUser($user); + + return $org; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(User $user): static + { + $this->user = $user; + + return $this; + } + + public function getTaxId(): ?string + { + return $this->taxId; + } + + public function setTaxId(string $taxId): static + { + $this->taxId = $taxId; + + return $this; + } + + public function getLegalName(): ?string + { + return $this->legalName; + } + + public function setLegalName(string $legalName): static + { + $this->legalName = $legalName; + + return $this; + } + + public function getBusinessName(): ?string + { + return $this->businessName; + } + + public function setBusinessName(?string $businessName): static + { + $this->businessName = $businessName; + + return $this; + } +} diff --git a/src/Entity/User/Person.php b/src/Entity/User/Person.php new file mode 100644 index 0000000..00b8b2d --- /dev/null +++ b/src/Entity/User/Person.php @@ -0,0 +1,101 @@ +setUser($user); + + return $person; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(User $user): static + { + $this->user = $user; + + return $this; + } + + public function getTaxId(): ?string + { + return $this->taxId; + } + + public function setTaxId(?string $taxId): static + { + $this->taxId = $taxId; + + return $this; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): static + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(string $lastName): static + { + $this->lastName = $lastName; + + return $this; + } +} diff --git a/src/Entity/User/User.php b/src/Entity/User/User.php index 630fce8..3e12ccc 100644 --- a/src/Entity/User/User.php +++ b/src/Entity/User/User.php @@ -13,6 +13,7 @@ use App\Repository\User\UserRepository; use AutoMapper\Attribute\MapProvider; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -30,7 +31,7 @@ use Symfony\Component\Security\Core\User\UserInterface; #[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Index(fields: ['migratedId'])] #[UniqueEntity('email', message: 'This email address is already registered.')] -#[UniqueEntity('username', message: 'This usernames already exists.')] +#[UniqueEntity('handle', message: 'This handle is already in use.')] class User implements UserInterface, PasswordAuthenticatedUserInterface, AccountingOwnerInterface { use MigratedEntity; @@ -47,23 +48,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account private ?string $email = null; /** - * Has this User confirmed their email address? + * @var string The user password */ #[ORM\Column] - private ?bool $emailConfirmed = null; + private ?string $password = null; /** * Human readable, non white space, unique string. */ #[Gedmo\Versioned] #[ORM\Column(length: 255, unique: true)] - private ?string $username = null; - - /** - * @var list The user roles. Admin only property. - */ - #[ORM\Column] - private array $roles = []; + private ?string $handle = null; #[ORM\OneToOne(inversedBy: 'user', cascade: ['persist'])] private ?Accounting $accounting = null; @@ -80,6 +75,24 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account #[ORM\OneToMany(mappedBy: 'owner', targetEntity: UserToken::class, orphanRemoval: true)] private Collection $tokens; + #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] + private ?Person $person = null; + + #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] + private ?Organization $organization = null; + + /** + * @var list The user roles. Admin only property. + */ + #[ORM\Column] + private array $roles = []; + + /** + * Has this User confirmed their email address? + */ + #[ORM\Column] + private ?bool $emailConfirmed = null; + /** * A flag determined by the platform for Users who are known to be active. */ @@ -87,35 +100,27 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account private ?bool $active = null; /** - * Path to the Users's avatar image. + * Is this user for an individual acting on their own or a larger group of individuals? */ - #[ORM\Column(length: 255, nullable: true)] + #[ORM\Column(enumType: UserType::class)] + private UserType $type = UserType::Individual; + + /** + * URL to the avatar image of this User. + */ + #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $avatar = null; - /** - * Conventional name of the person owning this User. - */ - #[ORM\Column(length: 255, nullable: true)] - private ?string $name = null; - - #[ORM\OneToOne(mappedBy: 'user', cascade: ['persist', 'remove'])] - private ?UserPersonal $personalData = null; - - /** - * @var string The user password - */ - #[ORM\Column] - private ?string $password = null; - public function __construct() { $this->accounting = Accounting::of($this); + $this->projects = new ArrayCollection(); + $this->tokens = new ArrayCollection(); + $this->person = Person::for($this); + $this->emailConfirmed = false; $this->active = false; - - $this->tokens = new ArrayCollection(); - $this->projects = new ArrayCollection(); } public function getId(): ?int @@ -130,7 +135,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account */ public function getUserIdentifier(): string { - return (string) $this->username; + return (string) $this->handle; } public function getEmail(): ?string @@ -145,57 +150,36 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - public function isEmailConfirmed(): ?bool - { - return $this->emailConfirmed; - } - - public function setEmailConfirmed(bool $emailConfirmed): static - { - $this->emailConfirmed = $emailConfirmed; - - return $this; - } - - public function getUsername(): ?string - { - return $this->username; - } - - public function setUsername(?string $username): static - { - $this->username = strtolower($username); - - return $this; - } - /** - * @see UserInterface - * - * @return list + * @see PasswordAuthenticatedUserInterface */ - public function getRoles(): array + public function getPassword(): string { - $roles = $this->roles; - // guarantee every user at least has ROLE_USER - $roles[] = 'ROLE_USER'; - - return array_unique($roles); + return $this->password; } - /** - * @param list $roles - */ - public function setRoles(array $roles): static + public function setPassword(string $password): static { - $this->roles = $roles; + $this->password = $password; return $this; } - public function hasRoles(array $roles): bool + public function eraseCredentials(): void { - return count(array_intersect($this->getRoles(), $roles)) > 0; + // If you store any temporary, sensitive data on the user, clear it here + } + + public function getHandle(): ?string + { + return $this->handle; + } + + public function setHandle(?string $handle): static + { + $this->handle = strtolower($handle); + + return $this; } public function getAccounting(): ?Accounting @@ -270,6 +254,81 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } + public function getPerson(): ?Person + { + return $this->person; + } + + public function setPerson(Person $person): static + { + // set the owning side of the relation if necessary + if ($person->getUser() !== $this) { + $person->setUser($this); + } + + $this->person = $person; + + return $this; + } + + public function getOrganization(): ?Organization + { + return $this->organization; + } + + public function setOrganization(Organization $organization): static + { + // set the owning side of the relation if necessary + if ($organization->getUser() !== $this) { + $organization->setUser($this); + } + + $this->organization = $organization; + + return $this; + } + + /** + * @see UserInterface + * + * @return list + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + /** + * @param list $roles + */ + public function setRoles(array $roles): static + { + $this->roles = $roles; + + return $this; + } + + public function hasRoles(array $roles): bool + { + return count(array_intersect($this->getRoles(), $roles)) > 0; + } + + public function isEmailConfirmed(): ?bool + { + return $this->emailConfirmed; + } + + public function setEmailConfirmed(bool $emailConfirmed): static + { + $this->emailConfirmed = $emailConfirmed; + + return $this; + } + public function isActive(): ?bool { return $this->active; @@ -282,6 +341,23 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } + public function getType(): UserType + { + return $this->type; + } + + public function isType(UserType $type): bool + { + return $type === $this->getType(); + } + + public function setType(UserType $type): static + { + $this->type = $type; + + return $this; + } + public function getAvatar(): ?string { return $this->avatar; @@ -293,53 +369,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Account return $this; } - - public function getName(): ?string - { - return $this->name; - } - - public function setName(?string $name): static - { - $this->name = $name; - - return $this; - } - - public function getPersonalData(): ?UserPersonal - { - return $this->personalData; - } - - public function setPersonalData(UserPersonal $personalData): static - { - // set the owning side of the relation if necessary - if ($personalData->getUser() !== $this) { - $personalData->setUser($this); - } - - $this->personalData = $personalData; - - return $this; - } - - /** - * @see PasswordAuthenticatedUserInterface - */ - public function getPassword(): string - { - return $this->password; - } - - public function setPassword(string $password): static - { - $this->password = $password; - - return $this; - } - - public function eraseCredentials(): void - { - // If you store any temporary, sensitive data on the user, clear it here - } } diff --git a/src/Entity/User/UserPersonal.php b/src/Entity/User/UserPersonal.php deleted file mode 100644 index 6417864..0000000 --- a/src/Entity/User/UserPersonal.php +++ /dev/null @@ -1,92 +0,0 @@ -user; - } - - public function setUser(User $user): static - { - $this->user = $user; - - return $this; - } - - public function getIdentityDocument(): ?string - { - return $this->identityDocument; - } - - public function setIdentityDocument(?string $identityDocument): static - { - $this->identityDocument = $identityDocument; - - return $this; - } - - public function getIndetityDocumentType(): ?string - { - return $this->identityDocumentType; - } - - public function setIndetityDocumentType(?string $identityDocumentType): static - { - $this->identityDocumentType = $identityDocumentType; - - return $this; - } - - public function getPostalCode(): ?string - { - return $this->postalCode; - } - - public function setPostalCode(?string $postalCode): static - { - $this->postalCode = $postalCode; - - return $this; - } -} diff --git a/src/Entity/User/UserType.php b/src/Entity/User/UserType.php new file mode 100644 index 0000000..adb8f8f --- /dev/null +++ b/src/Entity/User/UserType.php @@ -0,0 +1,16 @@ +andWhere(sprintf('%s.email LIKE :%s', $rootAlias, $parameterName)) - ->orWhere(sprintf('%s.name LIKE :%s', $rootAlias, $parameterName)) - ->orWhere(sprintf('%s.username LIKE :%s', $rootAlias, $parameterName)) + ->orWhere(sprintf('%s.handle LIKE :%s', $rootAlias, $parameterName)) ->setParameter($parameterName, $value); } @@ -55,7 +54,7 @@ final class UserQueryFilter extends AbstractFilter 'property' => $property, 'type' => Type::BUILTIN_TYPE_STRING, 'required' => false, - 'description' => 'Query Users by email, name or username. Fuzzy.', + 'description' => 'Query Users by email or handle. Fuzzy.', ]; } diff --git a/src/Mapping/Provider/OrganizationMapProvider.php b/src/Mapping/Provider/OrganizationMapProvider.php new file mode 100644 index 0000000..44cab35 --- /dev/null +++ b/src/Mapping/Provider/OrganizationMapProvider.php @@ -0,0 +1,22 @@ +orgRepository->find($source->user->id); + } +} diff --git a/src/Mapping/Provider/PersonMapProvider.php b/src/Mapping/Provider/PersonMapProvider.php new file mode 100644 index 0000000..7a60953 --- /dev/null +++ b/src/Mapping/Provider/PersonMapProvider.php @@ -0,0 +1,22 @@ +personRepository->find($source->user->id); + } +} diff --git a/src/Mapping/Transformer/UserDisplayNameMapTransformer.php b/src/Mapping/Transformer/UserDisplayNameMapTransformer.php new file mode 100644 index 0000000..187c505 --- /dev/null +++ b/src/Mapping/Transformer/UserDisplayNameMapTransformer.php @@ -0,0 +1,49 @@ +getType()) { + case UserType::Individual: + return $this->individualDisplayName($source); + case UserType::Organization: + return $this->organizationDisplayName($source); + } + } + + private function individualDisplayName(User $user): string + { + $person = $user->getPerson(); + + $name = \sprintf( + '%s %s', + $person->getFirstName(), + $person->getLastName() + ); + + return \trim($name); + } + + private function organizationDisplayName(User $user): string + { + $org = $user->getOrganization(); + + $name = $org->getBusinessName(); + + if (empty($name)) { + $name = $org->getBusinessName(); + } + + return $name ?? ''; + } +} diff --git a/src/OpenApi/OpenApiDescription.md b/src/OpenApi/OpenApiDescription.md index 3e53ec8..2417bbe 100644 --- a/src/OpenApi/OpenApiDescription.md +++ b/src/OpenApi/OpenApiDescription.md @@ -23,7 +23,7 @@ curl -X 'GET' \ UserTokens exist under an User's scope, when you obtain a token this will only grant you as much permissions as the User under which it was created. -To obtain a UserToken you must send a POST request to [/v4/user_tokens](/v4/user_tokens) with the User login credentials (username and password) in the payload. If the credentials are correct a UserToken will be created, the `token` property value of which you must include in future requests. +To obtain a UserToken you must send a POST request to [/v4/user_tokens](/v4/user_tokens) with the User login credentials (identifier and password) in the payload. If the credentials are correct a UserToken will be created, the `token` property value of which you must include in future requests. Users can delete UserTokens owned by them at any moment, revoking your application's access to the v4 API on their behalf. When a UserToken fails to authenticate a request you will receive a 401 Unauthorized response from the API, at which point your application must look to get a new UserToken from the User. diff --git a/src/OpenApi/RedocExtensionsTrait.php b/src/OpenApi/RedocExtensionsTrait.php index 00b0f66..ab60a4c 100644 --- a/src/OpenApi/RedocExtensionsTrait.php +++ b/src/OpenApi/RedocExtensionsTrait.php @@ -21,7 +21,7 @@ trait RedocExtensionsTrait $groups = [ [ 'name' => 'Users', - 'tags' => ['User', 'UserToken', 'UserPersonal'], + 'tags' => ['User', 'UserToken', 'Person', 'Organization'], ], [ 'name' => 'Projects', diff --git a/src/Repository/User/OrganizationRepository.php b/src/Repository/User/OrganizationRepository.php new file mode 100644 index 0000000..3215719 --- /dev/null +++ b/src/Repository/User/OrganizationRepository.php @@ -0,0 +1,43 @@ + + */ +class OrganizationRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Organization::class); + } + + // /** + // * @return Organization[] Returns an array of Organization objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('o') + // ->andWhere('o.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('o.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Organization + // { + // return $this->createQueryBuilder('o') + // ->andWhere('o.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/User/PersonRepository.php b/src/Repository/User/PersonRepository.php new file mode 100644 index 0000000..4b71c92 --- /dev/null +++ b/src/Repository/User/PersonRepository.php @@ -0,0 +1,23 @@ + + * + * @method Person|null find($id, $lockMode = null, $lockVersion = null) + * @method Person|null findOneBy(array $criteria, array $orderBy = null) + * @method Person[] findAll() + * @method Person[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class PersonRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Person::class); + } +} diff --git a/src/Repository/User/UserPersonalRepository.php b/src/Repository/User/UserPersonalRepository.php deleted file mode 100644 index 0bf629a..0000000 --- a/src/Repository/User/UserPersonalRepository.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * @method UserPersonal|null find($id, $lockMode = null, $lockVersion = null) - * @method UserPersonal|null findOneBy(array $criteria, array $orderBy = null) - * @method UserPersonal[] findAll() - * @method UserPersonal[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) - */ -class UserPersonalRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, UserPersonal::class); - } -} diff --git a/src/Repository/User/UserRepository.php b/src/Repository/User/UserRepository.php index 6e00a27..c69fb9c 100644 --- a/src/Repository/User/UserRepository.php +++ b/src/Repository/User/UserRepository.php @@ -44,14 +44,14 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader /** * Find an User by any of it's unique properties. * - * @param string $identifier An id, email or username + * @param string $identifier An id, email or handle */ public function findOneByIdentifier(string $identifier): ?User { return $this->createQueryBuilder('u') ->andWhere('u.id = :val') ->orWhere('u.email = :val') - ->orWhere('u.username = :val') + ->orWhere('u.handle = :val') ->setParameter('val', $identifier) ->getQuery() ->getOneOrNullResult(); @@ -66,6 +66,20 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader ->getOneOrNullResult(); } + /** + * @return User[] Returns an array of User objects + */ + public function findLike(string $handle): array + { + return $this->createQueryBuilder('u') + ->andWhere('u.handle LIKE :handle') + ->setParameter('handle', \sprintf('%%%s%%', $handle)) + ->orderBy('u.id', 'ASC') + ->getQuery() + ->getResult() + ; + } + // /** // * @return User[] Returns an array of User objects // */ diff --git a/src/Security/Voter/OrganizationVoter.php b/src/Security/Voter/OrganizationVoter.php new file mode 100644 index 0000000..f618f89 --- /dev/null +++ b/src/Security/Voter/OrganizationVoter.php @@ -0,0 +1,42 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + switch ($attribute) { + case self::EDIT: + return $subject->user->id === $user->getId(); + case self::VIEW: + return $user->hasRoles(['ROLE_ADMIN']) + || $subject->user->id === $user->getId(); + } + + return false; + } +} diff --git a/src/Security/Voter/PersonVoter.php b/src/Security/Voter/PersonVoter.php new file mode 100644 index 0000000..436275f --- /dev/null +++ b/src/Security/Voter/PersonVoter.php @@ -0,0 +1,42 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + switch ($attribute) { + case self::EDIT: + return $subject->user->id === $user->getId(); + case self::VIEW: + return $user->hasRoles(['ROLE_ADMIN']) + || $subject->user->id === $user->getId(); + } + + return false; + } +} diff --git a/src/Service/Auth/AuthService.php b/src/Service/Auth/AuthService.php index 8d507ee..1bd1d47 100644 --- a/src/Service/Auth/AuthService.php +++ b/src/Service/Auth/AuthService.php @@ -54,7 +54,7 @@ class AuthService } return $this->userRepository->findOneBy( - ['username' => $loggedInUser->getUserIdentifier()] + ['handle' => $loggedInUser->getUserIdentifier()] ); } } diff --git a/src/Service/UserService.php b/src/Service/UserService.php new file mode 100644 index 0000000..84633f7 --- /dev/null +++ b/src/Service/UserService.php @@ -0,0 +1,36 @@ +autoMapper->map($data, User::class); + + $user->setHandle($this->buildHandle($data)); $user->setPassword($this->userPasswordHasher->hashPassword($user, $data->password)); + + if ($user->isType(UserType::Organization)) { + $user->setOrganization(Organization::for($user)); + } + $user = $this->entityStateProcessor->process($user, $operation, $uriVariables, $context); return $this->autoMapper->map($user, UserApiResource::class); } + + private function buildHandle(UserSignupDto $data): string + { + $base = UserService::asHandle($data->email); + + $users = $this->userRepository->findLike($base); + $usersCount = \count($users); + + if ($usersCount < 1) { + return $base; + } + + return \sprintf('%s_%02d', $base, $usersCount + 1); + } } diff --git a/src/State/User/UserStateProcessor.php b/src/State/User/UserStateProcessor.php index 968063c..f0619f1 100644 --- a/src/State/User/UserStateProcessor.php +++ b/src/State/User/UserStateProcessor.php @@ -5,15 +5,18 @@ namespace App\State\User; use ApiPlatform\Doctrine\Common\State\PersistProcessor; use ApiPlatform\Metadata as API; use ApiPlatform\State\ProcessorInterface; +use App\Entity\User\Organization; +use App\Entity\User\User; +use App\Entity\User\UserType; +use App\Mapping\AutoMapper; use Symfony\Component\DependencyInjection\Attribute\Autowire; -use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; class UserStateProcessor implements ProcessorInterface { public function __construct( + private AutoMapper $autoMapper, #[Autowire(service: PersistProcessor::class)] - private ProcessorInterface $innerProcessor, - private UserPasswordHasherInterface $userPasswordHasher, + private ProcessorInterface $persistProcessor, ) {} /** @@ -23,6 +26,15 @@ class UserStateProcessor implements ProcessorInterface */ public function process(mixed $data, API\Operation $operation, array $uriVariables = [], array $context = []) { - return $data; + /** @var User */ + $user = $this->autoMapper->map($data, User::class); + + if ($user->isType(UserType::Organization) && $user->getOrganization() === null) { + $user->setOrganization(Organization::for($user)); + } + + $user = $this->persistProcessor->process($user, $operation, $uriVariables, $context); + + return $this->autoMapper->map($user, $data); } } diff --git a/tests/Entity/ProjectApiTest.php b/tests/Entity/ProjectApiTest.php index 9debc21..a89abbb 100644 --- a/tests/Entity/ProjectApiTest.php +++ b/tests/Entity/ProjectApiTest.php @@ -35,7 +35,7 @@ class ProjectApiTest extends ApiTestCase $this->assertJsonContains(['member' => []]); $owner = new User(); - $owner->setUsername('test_user'); + $owner->setHandle('test_user'); $owner->setEmail('testuser@example.com'); $owner->setPassword('projectapitestuserpassword'); diff --git a/tests/Gateway/Wallet/WalletServiceTest.php b/tests/Gateway/Wallet/WalletServiceTest.php index daa9126..d7c45c7 100644 --- a/tests/Gateway/Wallet/WalletServiceTest.php +++ b/tests/Gateway/Wallet/WalletServiceTest.php @@ -30,7 +30,7 @@ class WalletServiceTest extends KernelTestCase private function getUser(): User { $user = new User(); - $user->setUsername('wallettestuser'); + $user->setHandle('wallettestuser'); $user->setEmail('testuser@wallet.com'); $user->setPassword('wallettestpassword');