Merge pull request #70 from goteo/feat/user-legal

[feat] User legal
This commit is contained in:
2025-02-25 15:14:55 +01:00
committed by GitHub
31 changed files with 1089 additions and 476 deletions
Generated
+174 -171
View File
@@ -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": [],
+1 -1
View File
@@ -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)/
@@ -0,0 +1,73 @@
<?php
namespace App\ApiResource\User;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata as API;
use App\Entity\User\Organization;
use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Sometimes an Organization is behind a User account,
* we keep their data separated from the User record to allow for other User types,
* like the ones owned by an individual, to exist all at the same level.\
* \
* All Organization records are only visible to their respective owners and platform admins.
* Sensitive data is encrypted before being stored in the database.
*/
#[API\ApiResource(
shortName: 'Organization',
security: 'is_granted("ORGANIZATION_VIEW", object)',
stateOptions: new Options(entityClass: Organization::class),
provider: ApiResourceStateProvider::class,
processor: ApiResourceStateProcessor::class,
uriTemplate: '/users/{id}/organization',
uriVariables: [
'id' => 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;
}
@@ -0,0 +1,68 @@
<?php
namespace App\ApiResource\User;
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata as API;
use App\Entity\User\Person;
use App\State\ApiResourceStateProcessor;
use App\State\ApiResourceStateProvider;
/**
* Most times a real person is behind a User account,
* we keep their data separated from the User record to allow for other User types,
* like the ones owned by an organization, to exist all at the same level.\
* \
* All Person records are only visible to their respective owners and platform admins.
* Sensitive personal data is encrypted before being stored in the database.
*/
#[API\ApiResource(
shortName: 'Person',
security: 'is_granted("PERSON_VIEW", object)',
stateOptions: new Options(entityClass: Person::class),
provider: ApiResourceStateProvider::class,
processor: ApiResourceStateProcessor::class,
uriTemplate: '/users/{id}/person',
uriVariables: [
'id' => 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;
}
+51 -14
View File
@@ -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<int, string>
*/
#[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;
}
+8 -31
View File
@@ -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
+10 -9
View File
@@ -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;
}
+1 -1
View File
@@ -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;
+95
View File
@@ -0,0 +1,95 @@
<?php
namespace App\Entity\User;
use Ambta\DoctrineEncryptBundle\Configuration\Encrypted;
use App\Mapping\Provider\OrganizationMapProvider;
use App\Repository\User\OrganizationRepository;
use AutoMapper\Attribute\MapProvider;
use Doctrine\ORM\Mapping as ORM;
#[MapProvider(OrganizationMapProvider::class)]
#[ORM\Entity(repositoryClass: OrganizationRepository::class)]
class Organization
{
#[ORM\Id]
#[ORM\OneToOne(inversedBy: 'organization', cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
/**
* ID for tax purposes. e.g: NIF (formerly CIF), Umsatzsteuer-Id, EID, etc.
*/
#[ORM\Column(length: 255, nullable: true)]
#[Encrypted()]
private ?string $taxId = null;
/**
* Organization legal name before government,
* as it appears on legal documents issued by or for this organization.
*/
#[ORM\Column(length: 255, nullable: true)]
private ?string $legalName = null;
/**
* The name under which the organization presents itself.
*/
#[ORM\Column(length: 255, nullable: true)]
private ?string $businessName = null;
public static function for(User $user): Organization
{
$org = new Organization();
$org->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;
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace App\Entity\User;
use Ambta\DoctrineEncryptBundle\Configuration\Encrypted;
use App\Mapping\Provider\PersonMapProvider;
use App\Repository\User\PersonRepository;
use AutoMapper\Attribute\MapProvider;
use Doctrine\ORM\Mapping as ORM;
/**
* Person is the detailed data of an individual behind a User,
* we keep their data separated from the User record to allow for other User types,
* like the ones owned by an organization, to exist all at the same level.
* \
* Sensitive personal data is encrypted before being stored on the database.
*/
#[MapProvider(PersonMapProvider::class)]
#[ORM\Entity(repositoryClass: PersonRepository::class)]
class Person
{
#[ORM\Id]
#[ORM\OneToOne(inversedBy: 'person', cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
/**
* Personal ID for tax purposes. e.g: NIF, Steuernummer, TIN ID, etc.
*/
#[ORM\Column(length: 255, nullable: true)]
#[Encrypted]
private ?string $taxId = null;
/**
* First-part of the name of the person, usually the given name(s). e.g: John, Juan.
*/
#[ORM\Column(length: 255, nullable: true)]
private ?string $firstName = null;
/**
* Last-part of the name of the person, usually the family name(s). e.g: Smith, Herrera García.
*/
#[ORM\Column(length: 255, nullable: true)]
private ?string $lastName = null;
public static function for(User $user): Person
{
$person = new Person();
$person->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;
}
}
+147 -120
View File
@@ -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<string> 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<string> 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<string>
* @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<string> $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<string>
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
/**
* @param list<string> $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
}
}
-92
View File
@@ -1,92 +0,0 @@
<?php
namespace App\Entity\User;
use Ambta\DoctrineEncryptBundle\Configuration\Encrypted;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\User\UserPersonalRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* UserPersonal is the detailed data of a User.
*
* The personal data of a user will be kept encrypted
*/
#[ORM\Entity(repositoryClass: UserPersonalRepository::class)]
#[ApiResource]
class UserPersonal
{
#[ORM\Id]
#[ORM\OneToOne(inversedBy: 'personalData', cascade: ['persist', 'remove'])]
#[ORM\JoinColumn(nullable: false)]
private ?User $user = null;
/**
* The Identity Document number of the User.
*/
#[ORM\Column(length: 255, nullable: true)]
#[Encrypted]
private ?string $identityDocument = null;
/**
* The type of indentity document that the User has, ie: Passport, DNI, NIE, etc.
*/
#[ORM\Column(length: 255, nullable: true)]
#[Encrypted]
private ?string $identityDocumentType = null;
/**
* The Postal Code of where the User lives.
*/
#[ORM\Column(length: 255, nullable: true)]
#[Encrypted]
private ?string $postalCode = null;
public function getUser(): ?User
{
return $this->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;
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Entity\User;
enum UserType: string
{
/**
* People who sign-up on their own and do not represent any other legal or natural person.
*/
case Individual = 'individual';
/**
* Sometimes organizations want to sign-up and represent themselves.
*/
case Organization = 'organization';
}
+2 -3
View File
@@ -42,8 +42,7 @@ final class UserQueryFilter extends AbstractFilter
$queryBuilder
->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.',
];
}
@@ -0,0 +1,22 @@
<?php
namespace App\Mapping\Provider;
use App\ApiResource\User\OrganizationApiResource;
use App\Repository\User\OrganizationRepository;
use AutoMapper\Provider\ProviderInterface;
class OrganizationMapProvider implements ProviderInterface
{
public function __construct(
private OrganizationRepository $orgRepository,
) {}
/**
* @param OrganizationApiResource $source
*/
public function provide(string $targetType, mixed $source, array $context): object|array|null
{
return $this->orgRepository->find($source->user->id);
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Mapping\Provider;
use App\ApiResource\User\PersonApiResource;
use App\Repository\User\PersonRepository;
use AutoMapper\Provider\ProviderInterface;
class PersonMapProvider implements ProviderInterface
{
public function __construct(
private PersonRepository $personRepository,
) {}
/**
* @param PersonApiResource $source
*/
public function provide(string $targetType, mixed $source, array $context): object|array|null
{
return $this->personRepository->find($source->user->id);
}
}
@@ -0,0 +1,49 @@
<?php
namespace App\Mapping\Transformer;
use App\Entity\User\User;
use App\Entity\User\UserType;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;
class UserDisplayNameMapTransformer implements PropertyTransformerInterface
{
/**
* @param User $source
*/
public function transform(mixed $value, object|array $source, array $context): mixed
{
switch ($source->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 ?? '';
}
}
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -21,7 +21,7 @@ trait RedocExtensionsTrait
$groups = [
[
'name' => 'Users',
'tags' => ['User', 'UserToken', 'UserPersonal'],
'tags' => ['User', 'UserToken', 'Person', 'Organization'],
],
[
'name' => 'Projects',
@@ -0,0 +1,43 @@
<?php
namespace App\Repository\User;
use App\Entity\User\Organization;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Organization>
*/
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()
// ;
// }
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Repository\User;
use App\Entity\User\Person;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Person>
*
* @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);
}
}
@@ -1,23 +0,0 @@
<?php
namespace App\Repository\User;
use App\Entity\User\UserPersonal;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<UserPersonal>
*
* @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);
}
}
+16 -2
View File
@@ -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
// */
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Security\Voter;
use App\ApiResource\User\OrganizationApiResource;
use App\Entity\User\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class OrganizationVoter extends Voter
{
public const EDIT = 'ORGANIZATION_EDIT';
public const VIEW = 'ORGANIZATION_VIEW';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT, self::VIEW])
&& $subject instanceof OrganizationApiResource;
}
/**
* @param OrganizationApiResource $subject
*/
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->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;
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Security\Voter;
use App\ApiResource\User\PersonApiResource;
use App\Entity\User\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
final class PersonVoter extends Voter
{
public const EDIT = 'PERSON_EDIT';
public const VIEW = 'PERSON_VIEW';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [self::EDIT, self::VIEW])
&& $subject instanceof PersonApiResource;
}
/**
* @param PersonApiResource $subject
*/
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->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;
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ class AuthService
}
return $this->userRepository->findOneBy(
['username' => $loggedInUser->getUserIdentifier()]
['handle' => $loggedInUser->getUserIdentifier()]
);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Service;
class UserService
{
/**
* Normalizes a string into a handle-valid string.
*
* @return string A handle-valid string
*
* @throws \Exception If the given value could not be normalized into a compliant handle string
*/
public static function asHandle(string $value): string
{
// If email remove provider
if (\str_contains($value, '@') && \preg_match('/^[^@]+/', $value, $matches)) {
$value = $matches[0];
}
// Only lowercase a-z, numbers and underscore in user handles
$value = \preg_replace('/[^a-z0-9_]/', '_', \strtolower($value));
// Min length 4
$value = \str_pad($value, 4, '_');
// Max length 30
$value = \substr($value, 0, 30);
if (strlen(str_replace('_', '', $value)) < 1) {
throw new \Exception(\sprintf('The string \'%s\' could not be safely normalized for a handle', $value));
}
return $value;
}
}
+26
View File
@@ -6,8 +6,12 @@ use ApiPlatform\Metadata as API;
use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\User\UserApiResource;
use App\Dto\UserSignupDto;
use App\Entity\User\Organization;
use App\Entity\User\User;
use App\Entity\User\UserType;
use App\Mapping\AutoMapper;
use App\Repository\User\UserRepository;
use App\Service\UserService;
use App\State\EntityStateProcessor;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
@@ -16,6 +20,7 @@ final class UserSignupProcessor implements ProcessorInterface
public function __construct(
private AutoMapper $autoMapper,
private EntityStateProcessor $entityStateProcessor,
private UserRepository $userRepository,
private UserPasswordHasherInterface $userPasswordHasher,
) {}
@@ -28,9 +33,30 @@ final class UserSignupProcessor implements ProcessorInterface
{
/** @var User */
$user = $this->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);
}
}
+16 -4
View File
@@ -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);
}
}
+1 -1
View File
@@ -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');
+1 -1
View File
@@ -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');