From cd575f36c0fd76b8e0928df02bd73b70fa4bb04c Mon Sep 17 00:00:00 2001 From: Maximilian Pohl Date: Mon, 30 Sep 2024 13:25:43 +0200 Subject: [PATCH] Add user management --- ...cd1e0bedc713e772b644dc275643a5bef8f8a.json | 88 ++++ ...3192e6f338d89cf9be41c51a339eb42b3a474.json | 14 + ...30b94e24b7388414df2cf77645b0e69805ad4.json | 14 + ...8c06dfe060616bcafa5ddc38468ed964e8abd.json | 14 + ...b1186846f42d49bc190f07ca94b6a5e558602.json | 16 + ...24d47b3a4d324e74cac449b48a3f9f612afef.json | 15 + ...699aa9346c8cea7ecea9088ec5dd1539bc70c.json | 15 + ...4808c5e7957b9f907053013aa8147bffc7a8b.json | 15 + ...c75e8e552b90a3261e782d16d3b8805e80b98.json | 28 + ...321f97c8961774c043dc5ab643b5e6357eb35.json | 23 + ...48c54455fb4fdf08c0783ef3e42071a6b9d76.json | 14 + ...95505b9f10392316312567099acacca9bfc2c.json | 14 + ...ae078dfaab4a5b7cd831a9f813386acb52a37.json | 16 + ...5527cf66f5230a73659f8430c47d0b183b00d.json | 14 + ...c088d214a7304149575343d4ebb4e65eae495.json | 23 - ...050b1d90608a5ce1497bc45bab258ed8efa55.json | 23 - ...3651cb5dcfb45368207cf11f16bd30bd06428.json | 14 + ...02b686b7c464f4aa2b276369b3727127608d4.json | 86 ++++ ...e09c45ecec073fd32461071f3687ccd9c7e91.json | 14 + Cargo.lock | 33 ++ Cargo.toml | 1 + fixtures/users.sql | 10 +- migrations/20240826084440_initial_scheme.sql | 25 +- openadr-vtn/Cargo.toml | 3 +- openadr-vtn/src/api/auth.rs | 7 +- openadr-vtn/src/api/mod.rs | 1 + openadr-vtn/src/api/user.rs | 117 +++++ openadr-vtn/src/data_source/mod.rs | 53 +- openadr-vtn/src/data_source/postgres/user.rs | 487 +++++++++++++++--- openadr-vtn/src/error.rs | 30 +- openadr-vtn/src/main.rs | 2 +- openadr-vtn/src/state.rs | 21 +- openadr-wire/src/lib.rs | 2 +- 33 files changed, 1115 insertions(+), 137 deletions(-) create mode 100644 .sqlx/query-1bdd2974acddd89b65e87a7ea39cd1e0bedc713e772b644dc275643a5bef8f8a.json create mode 100644 .sqlx/query-3c89d5a7b353321d84c8e2177f83192e6f338d89cf9be41c51a339eb42b3a474.json create mode 100644 .sqlx/query-443f908e67a420dab5adb4ce6cf30b94e24b7388414df2cf77645b0e69805ad4.json create mode 100644 .sqlx/query-4b2ef0b91c7a5653deb318e196f8c06dfe060616bcafa5ddc38468ed964e8abd.json create mode 100644 .sqlx/query-4ccc3142896718d0032b9da549cb1186846f42d49bc190f07ca94b6a5e558602.json create mode 100644 .sqlx/query-528ef377a6005365ec7c8e8a8c324d47b3a4d324e74cac449b48a3f9f612afef.json create mode 100644 .sqlx/query-7c9a8981055380d8f02f608d435699aa9346c8cea7ecea9088ec5dd1539bc70c.json create mode 100644 .sqlx/query-8d40ce7830507cde558f39040094808c5e7957b9f907053013aa8147bffc7a8b.json create mode 100644 .sqlx/query-93be606a8174a16168120900a94c75e8e552b90a3261e782d16d3b8805e80b98.json create mode 100644 .sqlx/query-aed7cc730ddde8420590b38c674321f97c8961774c043dc5ab643b5e6357eb35.json create mode 100644 .sqlx/query-cfbc449106654ab6c245538238648c54455fb4fdf08c0783ef3e42071a6b9d76.json create mode 100644 .sqlx/query-d1245889f5dab872e8d1f9f178895505b9f10392316312567099acacca9bfc2c.json create mode 100644 .sqlx/query-d433cf76e8ec697933389b7d4dbae078dfaab4a5b7cd831a9f813386acb52a37.json create mode 100644 .sqlx/query-d52b6338b7923a007751ed891915527cf66f5230a73659f8430c47d0b183b00d.json delete mode 100644 .sqlx/query-d9da3079248311a250712457f57c088d214a7304149575343d4ebb4e65eae495.json delete mode 100644 .sqlx/query-e75e4d1b55ae024c8ce9771ecc9050b1d90608a5ce1497bc45bab258ed8efa55.json create mode 100644 .sqlx/query-f718be26da8cbaeae4c25baadca3651cb5dcfb45368207cf11f16bd30bd06428.json create mode 100644 .sqlx/query-f901477ce3cd03de812359382af02b686b7c464f4aa2b276369b3727127608d4.json create mode 100644 .sqlx/query-feaeaa22a9b48b647e30cad8582e09c45ecec073fd32461071f3687ccd9c7e91.json create mode 100644 openadr-vtn/src/api/user.rs diff --git a/.sqlx/query-1bdd2974acddd89b65e87a7ea39cd1e0bedc713e772b644dc275643a5bef8f8a.json b/.sqlx/query-1bdd2974acddd89b65e87a7ea39cd1e0bedc713e772b644dc275643a5bef8f8a.json new file mode 100644 index 0000000..8516828 --- /dev/null +++ b/.sqlx/query-1bdd2974acddd89b65e87a7ea39cd1e0bedc713e772b644dc275643a5bef8f8a.json @@ -0,0 +1,88 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT u.*,\n json_arrayagg(c.client_id) AS \"client_ids!\",\n b.user_id IS NOT NULL AS \"is_business_user!\",\n json_arrayagg(b.business_id NULL ON NULL) AS business_ids,\n ven.user_id IS NOT NULL AS \"is_ven_user!\",\n json_arrayagg(ven.ven_id) AS ven_ids,\n um.user_id IS NOT NULL AS \"is_user_manager!\",\n vm.user_id IS NOT NULL AS \"is_ven_manager!\"\n FROM \"user\" u\n LEFT JOIN user_credentials c ON c.user_id = u.id\n LEFT JOIN user_business b ON u.id = b.user_id\n LEFT JOIN user_manager um ON u.id = um.user_id\n LEFT JOIN user_ven ven ON u.id = ven.user_id\n LEFT JOIN ven_manager vm ON u.id = vm.user_id\n WHERE u.id = $1\n GROUP BY u.id, b.user_id, um.user_id, ven.user_id, vm.user_id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "reference", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "modified", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "client_ids!", + "type_info": "Json" + }, + { + "ordinal": 6, + "name": "is_business_user!", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "business_ids", + "type_info": "Json" + }, + { + "ordinal": 8, + "name": "is_ven_user!", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "ven_ids", + "type_info": "Json" + }, + { + "ordinal": 10, + "name": "is_user_manager!", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "is_ven_manager!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + null, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "1bdd2974acddd89b65e87a7ea39cd1e0bedc713e772b644dc275643a5bef8f8a" +} diff --git a/.sqlx/query-3c89d5a7b353321d84c8e2177f83192e6f338d89cf9be41c51a339eb42b3a474.json b/.sqlx/query-3c89d5a7b353321d84c8e2177f83192e6f338d89cf9be41c51a339eb42b3a474.json new file mode 100644 index 0000000..b9dc7d2 --- /dev/null +++ b/.sqlx/query-3c89d5a7b353321d84c8e2177f83192e6f338d89cf9be41c51a339eb42b3a474.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM user_manager WHERE user_id = $1 \n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "3c89d5a7b353321d84c8e2177f83192e6f338d89cf9be41c51a339eb42b3a474" +} diff --git a/.sqlx/query-443f908e67a420dab5adb4ce6cf30b94e24b7388414df2cf77645b0e69805ad4.json b/.sqlx/query-443f908e67a420dab5adb4ce6cf30b94e24b7388414df2cf77645b0e69805ad4.json new file mode 100644 index 0000000..5f29a95 --- /dev/null +++ b/.sqlx/query-443f908e67a420dab5adb4ce6cf30b94e24b7388414df2cf77645b0e69805ad4.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO user_business (user_id, business_id) VALUES ($1, NULL)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "443f908e67a420dab5adb4ce6cf30b94e24b7388414df2cf77645b0e69805ad4" +} diff --git a/.sqlx/query-4b2ef0b91c7a5653deb318e196f8c06dfe060616bcafa5ddc38468ed964e8abd.json b/.sqlx/query-4b2ef0b91c7a5653deb318e196f8c06dfe060616bcafa5ddc38468ed964e8abd.json new file mode 100644 index 0000000..5cb5585 --- /dev/null +++ b/.sqlx/query-4b2ef0b91c7a5653deb318e196f8c06dfe060616bcafa5ddc38468ed964e8abd.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM user_ven WHERE user_id = $1 \n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "4b2ef0b91c7a5653deb318e196f8c06dfe060616bcafa5ddc38468ed964e8abd" +} diff --git a/.sqlx/query-4ccc3142896718d0032b9da549cb1186846f42d49bc190f07ca94b6a5e558602.json b/.sqlx/query-4ccc3142896718d0032b9da549cb1186846f42d49bc190f07ca94b6a5e558602.json new file mode 100644 index 0000000..ec0b995 --- /dev/null +++ b/.sqlx/query-4ccc3142896718d0032b9da549cb1186846f42d49bc190f07ca94b6a5e558602.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE \"user\" SET\n reference = $2,\n description = $3,\n modified = now()\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "4ccc3142896718d0032b9da549cb1186846f42d49bc190f07ca94b6a5e558602" +} diff --git a/.sqlx/query-528ef377a6005365ec7c8e8a8c324d47b3a4d324e74cac449b48a3f9f612afef.json b/.sqlx/query-528ef377a6005365ec7c8e8a8c324d47b3a4d324e74cac449b48a3f9f612afef.json new file mode 100644 index 0000000..b2d28d6 --- /dev/null +++ b/.sqlx/query-528ef377a6005365ec7c8e8a8c324d47b3a4d324e74cac449b48a3f9f612afef.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO user_ven (user_id, ven_id) VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "528ef377a6005365ec7c8e8a8c324d47b3a4d324e74cac449b48a3f9f612afef" +} diff --git a/.sqlx/query-7c9a8981055380d8f02f608d435699aa9346c8cea7ecea9088ec5dd1539bc70c.json b/.sqlx/query-7c9a8981055380d8f02f608d435699aa9346c8cea7ecea9088ec5dd1539bc70c.json new file mode 100644 index 0000000..6c3a3f6 --- /dev/null +++ b/.sqlx/query-7c9a8981055380d8f02f608d435699aa9346c8cea7ecea9088ec5dd1539bc70c.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM user_credentials WHERE user_id = $1 AND client_id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "7c9a8981055380d8f02f608d435699aa9346c8cea7ecea9088ec5dd1539bc70c" +} diff --git a/.sqlx/query-8d40ce7830507cde558f39040094808c5e7957b9f907053013aa8147bffc7a8b.json b/.sqlx/query-8d40ce7830507cde558f39040094808c5e7957b9f907053013aa8147bffc7a8b.json new file mode 100644 index 0000000..b556b47 --- /dev/null +++ b/.sqlx/query-8d40ce7830507cde558f39040094808c5e7957b9f907053013aa8147bffc7a8b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO user_business (user_id, business_id) VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "8d40ce7830507cde558f39040094808c5e7957b9f907053013aa8147bffc7a8b" +} diff --git a/.sqlx/query-93be606a8174a16168120900a94c75e8e552b90a3261e782d16d3b8805e80b98.json b/.sqlx/query-93be606a8174a16168120900a94c75e8e552b90a3261e782d16d3b8805e80b98.json new file mode 100644 index 0000000..cab82dd --- /dev/null +++ b/.sqlx/query-93be606a8174a16168120900a94c75e8e552b90a3261e782d16d3b8805e80b98.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id,\n client_secret\n FROM \"user\"\n JOIN user_credentials ON user_id = id\n WHERE client_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "client_secret", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "93be606a8174a16168120900a94c75e8e552b90a3261e782d16d3b8805e80b98" +} diff --git a/.sqlx/query-aed7cc730ddde8420590b38c674321f97c8961774c043dc5ab643b5e6357eb35.json b/.sqlx/query-aed7cc730ddde8420590b38c674321f97c8961774c043dc5ab643b5e6357eb35.json new file mode 100644 index 0000000..7a161b7 --- /dev/null +++ b/.sqlx/query-aed7cc730ddde8420590b38c674321f97c8961774c043dc5ab643b5e6357eb35.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO \"user\" (id, reference, description, created, modified)\n VALUES (gen_random_uuid(), $1, $2, now(), now())\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "aed7cc730ddde8420590b38c674321f97c8961774c043dc5ab643b5e6357eb35" +} diff --git a/.sqlx/query-cfbc449106654ab6c245538238648c54455fb4fdf08c0783ef3e42071a6b9d76.json b/.sqlx/query-cfbc449106654ab6c245538238648c54455fb4fdf08c0783ef3e42071a6b9d76.json new file mode 100644 index 0000000..44cb1f8 --- /dev/null +++ b/.sqlx/query-cfbc449106654ab6c245538238648c54455fb4fdf08c0783ef3e42071a6b9d76.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO user_manager (user_id) VALUES ($1)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "cfbc449106654ab6c245538238648c54455fb4fdf08c0783ef3e42071a6b9d76" +} diff --git a/.sqlx/query-d1245889f5dab872e8d1f9f178895505b9f10392316312567099acacca9bfc2c.json b/.sqlx/query-d1245889f5dab872e8d1f9f178895505b9f10392316312567099acacca9bfc2c.json new file mode 100644 index 0000000..eb332b8 --- /dev/null +++ b/.sqlx/query-d1245889f5dab872e8d1f9f178895505b9f10392316312567099acacca9bfc2c.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM ven_manager WHERE user_id = $1 \n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "d1245889f5dab872e8d1f9f178895505b9f10392316312567099acacca9bfc2c" +} diff --git a/.sqlx/query-d433cf76e8ec697933389b7d4dbae078dfaab4a5b7cd831a9f813386acb52a37.json b/.sqlx/query-d433cf76e8ec697933389b7d4dbae078dfaab4a5b7cd831a9f813386acb52a37.json new file mode 100644 index 0000000..63cae35 --- /dev/null +++ b/.sqlx/query-d433cf76e8ec697933389b7d4dbae078dfaab4a5b7cd831a9f813386acb52a37.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO user_credentials \n (user_id, client_id, client_secret) \n VALUES \n ($1, $2, $3)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "d433cf76e8ec697933389b7d4dbae078dfaab4a5b7cd831a9f813386acb52a37" +} diff --git a/.sqlx/query-d52b6338b7923a007751ed891915527cf66f5230a73659f8430c47d0b183b00d.json b/.sqlx/query-d52b6338b7923a007751ed891915527cf66f5230a73659f8430c47d0b183b00d.json new file mode 100644 index 0000000..6123775 --- /dev/null +++ b/.sqlx/query-d52b6338b7923a007751ed891915527cf66f5230a73659f8430c47d0b183b00d.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO ven_manager (user_id) VALUES ($1)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "d52b6338b7923a007751ed891915527cf66f5230a73659f8430c47d0b183b00d" +} diff --git a/.sqlx/query-d9da3079248311a250712457f57c088d214a7304149575343d4ebb4e65eae495.json b/.sqlx/query-d9da3079248311a250712457f57c088d214a7304149575343d4ebb4e65eae495.json deleted file mode 100644 index d30aed0..0000000 --- a/.sqlx/query-d9da3079248311a250712457f57c088d214a7304149575343d4ebb4e65eae495.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT ven_id AS id\n FROM \"user\" u\n JOIN user_credentials c ON c.user_id = u.id\n JOIN user_ven v ON v.user_id = u.id \n WHERE client_id = $1\n AND client_secret = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "d9da3079248311a250712457f57c088d214a7304149575343d4ebb4e65eae495" -} diff --git a/.sqlx/query-e75e4d1b55ae024c8ce9771ecc9050b1d90608a5ce1497bc45bab258ed8efa55.json b/.sqlx/query-e75e4d1b55ae024c8ce9771ecc9050b1d90608a5ce1497bc45bab258ed8efa55.json deleted file mode 100644 index 729dda5..0000000 --- a/.sqlx/query-e75e4d1b55ae024c8ce9771ecc9050b1d90608a5ce1497bc45bab258ed8efa55.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT ub.business_id AS id \n FROM user_business ub\n JOIN \"user\" u ON u.id = ub.user_id\n JOIN user_credentials c ON c.user_id = u.id\n WHERE client_id = $1\n AND client_secret = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - true - ] - }, - "hash": "e75e4d1b55ae024c8ce9771ecc9050b1d90608a5ce1497bc45bab258ed8efa55" -} diff --git a/.sqlx/query-f718be26da8cbaeae4c25baadca3651cb5dcfb45368207cf11f16bd30bd06428.json b/.sqlx/query-f718be26da8cbaeae4c25baadca3651cb5dcfb45368207cf11f16bd30bd06428.json new file mode 100644 index 0000000..c509720 --- /dev/null +++ b/.sqlx/query-f718be26da8cbaeae4c25baadca3651cb5dcfb45368207cf11f16bd30bd06428.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM user_business WHERE user_id = $1 \n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "f718be26da8cbaeae4c25baadca3651cb5dcfb45368207cf11f16bd30bd06428" +} diff --git a/.sqlx/query-f901477ce3cd03de812359382af02b686b7c464f4aa2b276369b3727127608d4.json b/.sqlx/query-f901477ce3cd03de812359382af02b686b7c464f4aa2b276369b3727127608d4.json new file mode 100644 index 0000000..ae496bf --- /dev/null +++ b/.sqlx/query-f901477ce3cd03de812359382af02b686b7c464f4aa2b276369b3727127608d4.json @@ -0,0 +1,86 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT u.*,\n json_arrayagg(c.client_id) AS \"client_ids!\",\n b.user_id IS NOT NULL AS \"is_business_user!\",\n json_arrayagg(b.business_id NULL ON NULL) AS business_ids,\n ven.user_id IS NOT NULL AS \"is_ven_user!\",\n json_arrayagg(ven.ven_id) AS ven_ids,\n um.user_id IS NOT NULL AS \"is_user_manager!\",\n vm.user_id IS NOT NULL AS \"is_ven_manager!\"\n FROM \"user\" u\n LEFT JOIN user_credentials c ON c.user_id = u.id\n LEFT JOIN user_business b ON u.id = b.user_id\n LEFT JOIN user_manager um ON u.id = um.user_id\n LEFT JOIN user_ven ven ON u.id = ven.user_id\n LEFT JOIN ven_manager vm ON u.id = vm.user_id\n GROUP BY u.id, b.user_id, um.user_id, ven.user_id, vm.user_id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "reference", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "modified", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "client_ids!", + "type_info": "Json" + }, + { + "ordinal": 6, + "name": "is_business_user!", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "business_ids", + "type_info": "Json" + }, + { + "ordinal": 8, + "name": "is_ven_user!", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "ven_ids", + "type_info": "Json" + }, + { + "ordinal": 10, + "name": "is_user_manager!", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "is_ven_manager!", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + true, + false, + false, + null, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "f901477ce3cd03de812359382af02b686b7c464f4aa2b276369b3727127608d4" +} diff --git a/.sqlx/query-feaeaa22a9b48b647e30cad8582e09c45ecec073fd32461071f3687ccd9c7e91.json b/.sqlx/query-feaeaa22a9b48b647e30cad8582e09c45ecec073fd32461071f3687ccd9c7e91.json new file mode 100644 index 0000000..d4b04f1 --- /dev/null +++ b/.sqlx/query-feaeaa22a9b48b647e30cad8582e09c45ecec073fd32461071f3687ccd9c7e91.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM \"user\" WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "feaeaa22a9b48b647e30cad8582e09c45ecec073fd32461071f3687ccd9c7e91" +} diff --git a/Cargo.lock b/Cargo.lock index 207d9d2..150e1c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,18 @@ dependencies = [ "libc", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -224,6 +236,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1201,6 +1222,7 @@ dependencies = [ name = "openadr-vtn" version = "0.1.0" dependencies = [ + "argon2", "axum", "axum-extra", "chrono", @@ -1281,6 +1303,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" diff --git a/Cargo.toml b/Cargo.toml index be51c5a..8b13b7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,4 +55,5 @@ async-trait = "0.1.81" quickcheck = "1.0.3" sqlx = { version = "0.8.1", features = ["postgres", "runtime-tokio", "chrono", "migrate"] } +argon2 = "0.5.3" dotenvy = "0.15.7" \ No newline at end of file diff --git a/fixtures/users.sql b/fixtures/users.sql index 2b69ed0..8f803a3 100644 --- a/fixtures/users.sql +++ b/fixtures/users.sql @@ -1,10 +1,10 @@ -INSERT INTO "user" (id) -VALUES ('admin'); +INSERT INTO "user" (id, reference, description, created, modified) +VALUES ('admin', 'admin-ref', null, now(), now()); INSERT INTO user_business VALUES ('admin', NULL); INSERT INTO user_credentials (user_id, client_id, client_secret) -VALUES ('admin', 'admin', 'admin'); +VALUES ('admin', 'admin', '$argon2id$v=19$m=16,t=2,p=1$QmtwZnBPVnlIYkJTWUtHZg$lMxF0N+CeRa99UmzMaUKeg'); -- secret: admin -INSERT INTO "user" (id) -VALUES ('user-1'); \ No newline at end of file +INSERT INTO "user" (id, reference, description, created, modified) +VALUES ('user-1', 'user-1-ref', null, now(), now()); \ No newline at end of file diff --git a/migrations/20240826084440_initial_scheme.sql b/migrations/20240826084440_initial_scheme.sql index 66866f4..aa6dc58 100644 --- a/migrations/20240826084440_initial_scheme.sql +++ b/migrations/20240826084440_initial_scheme.sql @@ -75,18 +75,17 @@ create unique index report_report_name_uindex create table "user" ( - -- TODO maybe add a (human friendly) name or reference - id text not null - constraint user_pk - primary key + id text primary key, + reference text not null, + description text, + created timestamptz not null, + modified timestamptz not null ); create table user_credentials ( user_id text not null references "user" (id) on delete cascade, - client_id text not null - constraint user_credentials_pk - primary key, + client_id text primary key, client_secret text not null -- TODO maybe the credentials require their own role? ); @@ -146,4 +145,14 @@ create unique index null_test_user_business where business_id is null; create unique index uindex_user_business - on user_business (user_id, business_id); \ No newline at end of file + on user_business (user_id, business_id); + +create table ven_manager +( + user_id text primary key references "user" (id) on delete cascade +); + +create table user_manager +( + user_id text primary key references "user" (id) on delete cascade +); \ No newline at end of file diff --git a/openadr-vtn/Cargo.toml b/openadr-vtn/Cargo.toml index b1a5a90..157720d 100644 --- a/openadr-vtn/Cargo.toml +++ b/openadr-vtn/Cargo.toml @@ -38,6 +38,7 @@ chrono.workspace = true thiserror.workspace = true sqlx = {workspace = true, optional = true} +argon2 = {workspace = true, optional = true} dotenvy = {workspace = true, optional = true} [dev-dependencies] @@ -46,4 +47,4 @@ tokio = { workspace = true, features = ["full", "test-util"] } [features] default = ["postgres", "live-db-test"] live-db-test = ["postgres"] -postgres = ["sqlx/postgres", "dep:dotenvy"] \ No newline at end of file +postgres = ["sqlx/postgres", "dep:dotenvy", "dep:argon2"] \ No newline at end of file diff --git a/openadr-vtn/src/api/auth.rs b/openadr-vtn/src/api/auth.rs index 610ca5d..348c490 100644 --- a/openadr-vtn/src/api/auth.rs +++ b/openadr-vtn/src/api/auth.rs @@ -74,7 +74,7 @@ impl IntoResponse for AccessTokenResponse { } /// RFC 6749 client credentials grant flow -pub async fn token( +pub(crate) async fn token( State(auth_source): State>, State(jwt_manager): State>, authorization: Option>>, @@ -117,7 +117,10 @@ pub async fn token( }; // check that the client_id and client_secret are valid - let Some(user) = auth_source.get_user(client_id, client_secret).await else { + let Some(user) = auth_source + .check_credentials(client_id, client_secret) + .await + else { return Err(OAuthError::new(OAuthErrorType::InvalidClient) .with_description("Invalid client_id or client_secret".to_string()) .into()); diff --git a/openadr-vtn/src/api/mod.rs b/openadr-vtn/src/api/mod.rs index 124d8a7..b8b4e33 100644 --- a/openadr-vtn/src/api/mod.rs +++ b/openadr-vtn/src/api/mod.rs @@ -13,6 +13,7 @@ pub mod event; pub mod program; pub mod report; pub mod resource; +pub mod user; pub mod ven; pub type AppResponse = Result, AppError>; diff --git a/openadr-vtn/src/api/user.rs b/openadr-vtn/src/api/user.rs new file mode 100644 index 0000000..bcee9bc --- /dev/null +++ b/openadr-vtn/src/api/user.rs @@ -0,0 +1,117 @@ +use crate::{ + api::AppResponse, + data_source::{AuthSource, UserDetails}, + jwt::{AuthRole, UserManagerUser}, +}; +use axum::{ + extract::{Path, State}, + Json, +}; +use serde_with::serde_derive::Deserialize; +use std::sync::Arc; +use tracing::{info, trace}; + +#[derive(Deserialize)] +pub struct NewUser { + reference: String, + description: Option, + roles: Vec, +} + +#[derive(Deserialize)] +pub struct NewCredential { + client_id: String, + client_secret: String, +} + +pub async fn get_all( + State(auth_source): State>, + UserManagerUser(_): UserManagerUser, +) -> AppResponse> { + let users = auth_source.get_all_users().await?; + + trace!("received {} users", users.len()); + Ok(Json(users)) +} + +pub async fn get( + State(auth_source): State>, + Path(id): Path, + UserManagerUser(_): UserManagerUser, +) -> AppResponse { + let user = auth_source.get_user(&id).await?; + trace!(user_id = user.id(), "received user"); + Ok(Json(user)) +} + +pub async fn add_user( + State(auth_source): State>, + UserManagerUser(_): UserManagerUser, + Json(new_user): Json, +) -> AppResponse { + let user = auth_source + .add_user( + &new_user.reference, + new_user.description.as_deref(), + &new_user.roles, + ) + .await?; + info!(user_id = user.id(), "created new user"); + Ok(Json(user)) +} + +pub async fn add_credential( + State(auth_source): State>, + Path(id): Path, + UserManagerUser(_): UserManagerUser, + Json(new): Json, +) -> AppResponse { + let user = auth_source + .add_credentials(&id, &new.client_id, &new.client_secret) + .await?; + info!( + user_id = id, + client_id = new.client_id, + "created new credential for user" + ); + Ok(Json(user)) +} + +pub async fn edit( + State(auth_source): State>, + Path(id): Path, + UserManagerUser(_): UserManagerUser, + Json(modified): Json, +) -> AppResponse { + let user = auth_source + .edit_user( + &id, + &modified.reference, + modified.description.as_deref(), + &modified.roles, + ) + .await?; + + info!(user_id = user.id(), "updated user"); + Ok(Json(user)) +} + +pub async fn delete_user( + State(auth_source): State>, + Path(id): Path, + UserManagerUser(_): UserManagerUser, +) -> AppResponse { + let user = auth_source.remove_user(&id).await?; + info!(user_id = user.id(), "deleted user"); + Ok(Json(user)) +} + +pub async fn delete_credential( + State(auth_source): State>, + Path((user_id, client_id)): Path<(String, String)>, + UserManagerUser(_): UserManagerUser, +) -> AppResponse { + let user = auth_source.remove_credentials(&user_id, &client_id).await?; + info!(user_id = user.id(), client_id, "deleted credential"); + Ok(Json(user)) +} diff --git a/openadr-vtn/src/data_source/mod.rs b/openadr-vtn/src/data_source/mod.rs index 33f2f9e..a28e9e6 100644 --- a/openadr-vtn/src/data_source/mod.rs +++ b/openadr-vtn/src/data_source/mod.rs @@ -2,6 +2,7 @@ mod postgres; use axum::async_trait; +use chrono::{DateTime, Utc}; use openadr_wire::{ event::{EventContent, EventId}, program::{ProgramContent, ProgramId}, @@ -10,10 +11,10 @@ use openadr_wire::{ ven::{Ven, VenContent, VenId}, Event, Program, Report, }; -use std::sync::Arc; - #[cfg(feature = "postgres")] pub use postgres::PostgresStorage; +use serde::Serialize; +use std::sync::Arc; use crate::{ error::AppError, @@ -189,9 +190,55 @@ pub trait ResourceCrud: { } +#[derive(Serialize)] +pub struct UserDetails { + id: String, + reference: String, + description: Option, + roles: Vec, + client_ids: Vec, + #[serde(with = "openadr_wire::serde_rfc3339")] + created: DateTime, + #[serde(with = "openadr_wire::serde_rfc3339")] + modified: DateTime, +} + +impl UserDetails { + pub fn id(&self) -> &str { + &self.id + } +} + #[async_trait] pub trait AuthSource: Send + Sync + 'static { - async fn get_user(&self, client_id: &str, client_secret: &str) -> Option; + async fn check_credentials(&self, client_id: &str, client_secret: &str) -> Option; + async fn get_user(&self, user_id: &str) -> Result; + async fn get_all_users(&self) -> Result, AppError>; + async fn add_user( + &self, + reference: &str, + description: Option<&str>, + roles: &[AuthRole], + ) -> Result; + async fn add_credentials( + &self, + user_id: &str, + client_id: &str, + client_secret: &str, + ) -> Result; + async fn remove_credentials( + &self, + user_id: &str, + client_id: &str, + ) -> Result; + async fn remove_user(&self, user_id: &str) -> Result; + async fn edit_user( + &self, + user_id: &str, + reference: &str, + description: Option<&str>, + roles: &[AuthRole], + ) -> Result; } pub trait DataSource: Send + Sync + 'static { diff --git a/openadr-vtn/src/data_source/postgres/user.rs b/openadr-vtn/src/data_source/postgres/user.rs index 493ae90..a4c4c30 100644 --- a/openadr-vtn/src/data_source/postgres/user.rs +++ b/openadr-vtn/src/data_source/postgres/user.rs @@ -1,10 +1,16 @@ use crate::{ - data_source::{postgres::PgId, AuthInfo, AuthSource}, + data_source::{postgres::PgId, AuthInfo, AuthSource, UserDetails}, + error::AppError, jwt::AuthRole, }; +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHash, PasswordHasher, PasswordVerifier, +}; use axum::async_trait; -use openadr_wire::IdentifierError; -use sqlx::PgPool; +use chrono::{DateTime, Utc}; +use sqlx::{PgConnection, PgPool}; +use tracing::warn; pub struct PgAuthSource { db: PgPool, @@ -17,79 +23,438 @@ impl From for PgAuthSource { } #[derive(Debug)] -struct MaybePgId { - id: Option, +struct IntermediateUser { + id: String, + reference: String, + description: Option, + client_ids: serde_json::Value, + created: DateTime, + modified: DateTime, + is_business_user: bool, + business_ids: serde_json::Value, + is_ven_user: bool, + ven_ids: serde_json::Value, + is_user_manager: bool, + is_ven_manager: bool, +} + +impl TryFrom for UserDetails { + type Error = AppError; + + fn try_from(u: IntermediateUser) -> Result { + let mut roles = Vec::new(); + if u.is_business_user { + let business_ids: Vec> = serde_json::from_value(u.business_ids) + .map_err(|err| { + warn!( + user_id = u.id, + "failed to deserialize user associated businesses: {err}" + ); + AppError::SerdeJsonInternalServerError(err) + })?; + roles.append( + &mut business_ids + .into_iter() + .map(|id| match id { + None => Ok(AuthRole::AnyBusiness), + Some(id) => Ok(AuthRole::Business(id)), + }) + .collect::, AppError>>()?, + ) + } + + if u.is_ven_user { + let ven_ids: Vec = serde_json::from_value(u.ven_ids).map_err(|err| { + warn!( + user_id = u.id, + "failed to deserialize user associated vens: {err}" + ); + AppError::SerdeJsonInternalServerError(err) + })?; + roles.append( + &mut ven_ids + .into_iter() + .map(|id| Ok(AuthRole::VEN(id.parse()?))) + .collect::, AppError>>()?, + ) + } + + if u.is_user_manager { + roles.push(AuthRole::UserManager); + } + + if u.is_ven_manager { + roles.push(AuthRole::VenManager) + } + + let client_ids = serde_json::from_value(u.client_ids).map_err(|err| { + warn!( + user_id = u.id, + "failed to deserialize user client ids: {err}" + ); + AppError::SerdeJsonInternalServerError(err) + })?; + + Ok(Self { + id: u.id, + reference: u.reference, + description: u.description, + roles, + client_ids, + created: u.created, + modified: u.modified, + }) + } +} + +struct IdAndSecret { + id: String, + client_secret: String, } #[async_trait] impl AuthSource for PgAuthSource { - async fn get_user(&self, client_id: &str, client_secret: &str) -> Option { - let vens = sqlx::query_as!( - PgId, + async fn check_credentials(&self, client_id: &str, client_secret: &str) -> Option { + let mut tx = self + .db + .begin() + .await + .inspect_err(|err| warn!(client_id, "failed to open transaction: {err}")) + .ok()?; + + let db_entry = sqlx::query_as!( + IdAndSecret, r#" - SELECT ven_id AS id - FROM "user" u - JOIN user_credentials c ON c.user_id = u.id - JOIN user_ven v ON v.user_id = u.id + SELECT id, + client_secret + FROM "user" + JOIN user_credentials ON user_id = id WHERE client_id = $1 - AND client_secret = $2 "#, client_id, - client_secret ) - .fetch_all(&self.db) + .fetch_one(&mut *tx) .await - .ok(); + .ok()?; + + let parsed_hash = PasswordHash::new(&db_entry.client_secret) + .inspect_err(|err| warn!("Failed to parse client_secret_hash in DB: {}", err)) + .ok()?; + + Argon2::default() + .verify_password(client_secret.as_bytes(), &parsed_hash) + .ok()?; + + let user = Self::get_user(&mut tx, &db_entry.id) + .await + .inspect_err(|err| warn!(client_id, "error fetching user: {err}")) + .ok()?; + + Some(AuthInfo { + client_id: client_id.to_string(), + roles: user.roles, + }) + } - let businesses = sqlx::query_as!( - MaybePgId, + async fn get_user(&self, user_id: &str) -> Result { + let mut tx = self.db.begin().await?; + Self::get_user(&mut tx, user_id).await + } + + async fn get_all_users(&self) -> Result, AppError> { + sqlx::query_as!( + IntermediateUser, r#" - SELECT ub.business_id AS id - FROM user_business ub - JOIN "user" u ON u.id = ub.user_id - JOIN user_credentials c ON c.user_id = u.id - WHERE client_id = $1 - AND client_secret = $2 + SELECT u.*, + json_arrayagg(c.client_id) AS "client_ids!", + b.user_id IS NOT NULL AS "is_business_user!", + json_arrayagg(b.business_id NULL ON NULL) AS business_ids, + ven.user_id IS NOT NULL AS "is_ven_user!", + json_arrayagg(ven.ven_id) AS ven_ids, + um.user_id IS NOT NULL AS "is_user_manager!", + vm.user_id IS NOT NULL AS "is_ven_manager!" + FROM "user" u + LEFT JOIN user_credentials c ON c.user_id = u.id + LEFT JOIN user_business b ON u.id = b.user_id + LEFT JOIN user_manager um ON u.id = um.user_id + LEFT JOIN user_ven ven ON u.id = ven.user_id + LEFT JOIN ven_manager vm ON u.id = vm.user_id + GROUP BY u.id, b.user_id, um.user_id, ven.user_id, vm.user_id "#, - client_id, - client_secret ) .fetch_all(&self.db) - .await - .ok(); - - let mut ven_roles = vens - .and_then(|vens| { - vens.into_iter() - .map(|ven| Ok(AuthRole::VEN(ven.id.parse()?))) - .collect::, IdentifierError>>() - .ok() - }) - .unwrap_or_default(); - - let mut business_roles = businesses - .map(|vens| { - vens.into_iter() - .map(|ven| { - if let Some(id) = ven.id { - AuthRole::Business(id) - } else { - AuthRole::AnyBusiness - } - }) - .collect::>() - }) - .unwrap_or_default(); - - ven_roles.append(&mut business_roles); - - if ven_roles.is_empty() { - None - } else { - Some(AuthInfo { - client_id: client_id.to_string(), - roles: ven_roles, - }) + .await? + .into_iter() + .map(TryInto::try_into) + .collect() + } + + async fn add_user( + &self, + reference: &str, + description: Option<&str>, + roles: &[AuthRole], + ) -> Result { + let mut tx = self.db.begin().await?; + + let user = sqlx::query_as!( + PgId, + r#" + INSERT INTO "user" (id, reference, description, created, modified) + VALUES (gen_random_uuid(), $1, $2, now(), now()) + RETURNING id + "#, + reference, + description + ) + .fetch_one(&mut *tx) + .await?; + + for role in roles { + Self::add_role(&mut tx, &user.id, role) + .await + .inspect_err(|err| { + warn!( + "Failed to add role {:?} for new user {:?}: {}", + role, user, err + ) + })?; } + + let user = Self::get_user(&mut tx, &user.id) + .await + .inspect_err(|err| warn!("cannot find user just created: {}", err))?; + + tx.commit().await?; + Ok(user) + } + + async fn add_credentials( + &self, + user_id: &str, + client_id: &str, + client_secret: &str, + ) -> Result { + let salt = SaltString::generate(&mut OsRng); + + let argon2 = Argon2::default(); + let hash = argon2 + .hash_password(client_secret.as_bytes(), &salt)? + .to_string(); + + let mut tx = self.db.begin().await?; + + sqlx::query!( + r#" + INSERT INTO user_credentials + (user_id, client_id, client_secret) + VALUES + ($1, $2, $3) + "#, + user_id, + client_id, + &hash + ) + .execute(&mut *tx) + .await?; + let user = Self::get_user(&mut tx, user_id).await?; + tx.commit().await?; + + Ok(user) + } + + async fn remove_credentials( + &self, + user_id: &str, + client_id: &str, + ) -> Result { + let mut tx = self.db.begin().await?; + sqlx::query!( + r#" + DELETE FROM user_credentials WHERE user_id = $1 AND client_id = $2 + "#, + user_id, + client_id + ) + .execute(&mut *tx) + .await?; + let user = Self::get_user(&mut tx, user_id).await?; + tx.commit().await?; + Ok(user) + } + + async fn remove_user(&self, user_id: &str) -> Result { + let mut tx = self.db.begin().await?; + let user = Self::get_user(&mut tx, user_id).await?; + sqlx::query!( + r#" + DELETE FROM "user" WHERE id = $1 + "#, + user_id + ) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + Ok(user) + } + + async fn edit_user( + &self, + user_id: &str, + reference: &str, + description: Option<&str>, + roles: &[AuthRole], + ) -> Result { + let mut tx = self.db.begin().await?; + + sqlx::query!( + r#" + UPDATE "user" SET + reference = $2, + description = $3, + modified = now() + WHERE id = $1 + "#, + user_id, + reference, + description + ) + .execute(&mut *tx) + .await?; + + Self::delete_all_roles(&mut tx, user_id).await?; + + for role in roles { + Self::add_role(&mut tx, user_id, role) + .await + .inspect_err(|err| { + warn!( + "Failed to add role {:?} for updated user {:?}: {}", + role, user_id, err + ) + })?; + } + let user = Self::get_user(&mut tx, user_id) + .await + .inspect_err(|err| warn!("cannot find user just updated: {}", err))?; + + tx.commit().await?; + Ok(user) + } +} + +impl PgAuthSource { + async fn delete_all_roles(db: &mut PgConnection, user_id: &str) -> Result<(), AppError> { + sqlx::query!( + r#" + DELETE FROM user_ven WHERE user_id = $1 + "#, + user_id + ) + .execute(&mut *db) + .await?; + + sqlx::query!( + r#" + DELETE FROM user_business WHERE user_id = $1 + "#, + user_id + ) + .execute(&mut *db) + .await?; + + sqlx::query!( + r#" + DELETE FROM ven_manager WHERE user_id = $1 + "#, + user_id + ) + .execute(&mut *db) + .await?; + + sqlx::query!( + r#" + DELETE FROM user_manager WHERE user_id = $1 + "#, + user_id + ) + .execute(&mut *db) + .await?; + + Ok(()) + } + + async fn add_role( + tx: &mut PgConnection, + user_id: &str, + role: &AuthRole, + ) -> Result<(), AppError> { + match role { + AuthRole::Business(b_id) => sqlx::query!( + r#" + INSERT INTO user_business (user_id, business_id) VALUES ($1, $2) + "#, + user_id, + b_id + ), + AuthRole::AnyBusiness => sqlx::query!( + r#" + INSERT INTO user_business (user_id, business_id) VALUES ($1, NULL) + "#, + user_id + ), + AuthRole::VEN(v_id) => sqlx::query!( + r#" + INSERT INTO user_ven (user_id, ven_id) VALUES ($1, $2) + "#, + user_id, + v_id.as_str() + ), + AuthRole::VenManager => sqlx::query!( + r#" + INSERT INTO ven_manager (user_id) VALUES ($1) + "#, + user_id + ), + AuthRole::UserManager => sqlx::query!( + r#" + INSERT INTO user_manager (user_id) VALUES ($1) + "#, + user_id + ), + } + .execute(&mut *tx) + .await?; + + Ok(()) + } + + async fn get_user(tx: &mut PgConnection, user_id: &str) -> Result { + sqlx::query_as!( + IntermediateUser, + r#" + SELECT u.*, + json_arrayagg(c.client_id) AS "client_ids!", + b.user_id IS NOT NULL AS "is_business_user!", + json_arrayagg(b.business_id NULL ON NULL) AS business_ids, + ven.user_id IS NOT NULL AS "is_ven_user!", + json_arrayagg(ven.ven_id) AS ven_ids, + um.user_id IS NOT NULL AS "is_user_manager!", + vm.user_id IS NOT NULL AS "is_ven_manager!" + FROM "user" u + LEFT JOIN user_credentials c ON c.user_id = u.id + LEFT JOIN user_business b ON u.id = b.user_id + LEFT JOIN user_manager um ON u.id = um.user_id + LEFT JOIN user_ven ven ON u.id = ven.user_id + LEFT JOIN ven_manager vm ON u.id = vm.user_id + WHERE u.id = $1 + GROUP BY u.id, b.user_id, um.user_id, ven.user_id, vm.user_id + "#, + user_id + ) + .fetch_one(&mut *tx) + .await? + .try_into() } } diff --git a/openadr-vtn/src/error.rs b/openadr-vtn/src/error.rs index 5a83935..a74ba0c 100644 --- a/openadr-vtn/src/error.rs +++ b/openadr-vtn/src/error.rs @@ -1,3 +1,4 @@ +use argon2::password_hash; use axum::{ extract::rejection::JsonRejection, http::StatusCode, @@ -33,7 +34,7 @@ pub enum AppError { Conflict(String, Option>), #[cfg(feature = "sqlx")] #[error("Unprocessable Content: {0}")] - ForeignKeyConstrainstViolated(String, Option>), + ForeignKeyConstraintViolated(String, Option>), #[error("Authentication error: {0}")] Auth(String), #[cfg(feature = "sqlx")] @@ -49,6 +50,9 @@ pub enum AppError { Identifier(#[from] IdentifierError), #[error("Method not allowed")] MethodNotAllowed, + #[cfg(feature = "sqlx")] + #[error("Password Hash error: {0}")] + PasswordHashError(password_hash::Error), } #[cfg(feature = "sqlx")] @@ -60,7 +64,7 @@ impl From for AppError { Self::Conflict("Conflict".to_string(), Some(err)) } sqlx::Error::Database(err) if err.is_foreign_key_violation() => { - Self::ForeignKeyConstrainstViolated( + Self::ForeignKeyConstraintViolated( "A foreign key constraint is violated".to_string(), Some(err), ) @@ -70,6 +74,12 @@ impl From for AppError { } } +impl From for AppError { + fn from(hash_err: password_hash::Error) -> Self { + Self::PasswordHashError(hash_err) + } +} + impl AppError { fn into_problem(self) -> Problem { let reference = Uuid::new_v4(); @@ -202,7 +212,7 @@ impl AppError { r#type: Default::default(), title: Some(StatusCode::INTERNAL_SERVER_ERROR.to_string()), status: StatusCode::INTERNAL_SERVER_ERROR, - detail: Some(err.to_string()), + detail: None, instance: Some(reference.to_string()), } } @@ -231,7 +241,7 @@ impl AppError { } } #[cfg(feature = "sqlx")] - AppError::ForeignKeyConstrainstViolated(err, db_err) => { + AppError::ForeignKeyConstraintViolated(err, db_err) => { trace!(%reference, "Unprocessable Content: {}, DB details: {:?}", err, @@ -257,6 +267,18 @@ impl AppError { instance: Some(reference.to_string()), } } + AppError::PasswordHashError(err) => { + warn!(%reference, + "Password hash error: {}", + err); + Problem { + r#type: Default::default(), + title: Some(StatusCode::INTERNAL_SERVER_ERROR.to_string()), + status: StatusCode::INTERNAL_SERVER_ERROR, + detail: Some("An internal error occurred".to_string()), + instance: Some(reference.to_string()), + } + } } } } diff --git a/openadr-vtn/src/main.rs b/openadr-vtn/src/main.rs index 7ae1b5d..8de0a3b 100644 --- a/openadr-vtn/src/main.rs +++ b/openadr-vtn/src/main.rs @@ -9,7 +9,7 @@ use openadr_vtn::{jwt::JwtManager, state::AppState}; #[tokio::main] async fn main() { tracing_subscriber::registry() - .with(fmt::layer()) + .with(fmt::layer().with_file(true).with_line_number(true)) .with(EnvFilter::from_default_env()) .init(); diff --git a/openadr-vtn/src/state.rs b/openadr-vtn/src/state.rs index 07cfad4..d8fef83 100644 --- a/openadr-vtn/src/state.rs +++ b/openadr-vtn/src/state.rs @@ -10,9 +10,13 @@ use axum::{ middleware, middleware::Next, response::IntoResponse, + routing::{delete, get, post}, }; use reqwest::StatusCode; use std::sync::Arc; +use tower_http::trace::TraceLayer; + +use crate::api::{auth, event, program, report, resource, user, ven}; #[derive(Clone, FromRef)] pub struct AppState { @@ -29,11 +33,6 @@ impl AppState { } fn router_without_state() -> axum::Router { - use axum::routing::{get, post}; - use tower_http::trace::TraceLayer; - - use crate::api::{auth, event, program, report, resource, ven}; - axum::Router::new() .route("/programs", get(program::get_all).post(program::add)) .route( @@ -67,6 +66,18 @@ impl AppState { ) .route("/auth/register", post(auth::register)) .route("/auth/token", post(auth::token)) + .route("/users", get(user::get_all).post(user::add_user)) + .route( + "/users/:id", + get(user::get) + .put(user::edit) + .delete(user::delete_user) + .post(user::add_credential), + ) + .route( + "/users/:user_id/:client_id", + delete(user::delete_credential), + ) .layer(middleware::from_fn(method_not_allowed)) .layer(TraceLayer::new_for_http()) } diff --git a/openadr-wire/src/lib.rs b/openadr-wire/src/lib.rs index 0508fb0..2e47127 100644 --- a/openadr-wire/src/lib.rs +++ b/openadr-wire/src/lib.rs @@ -23,7 +23,7 @@ pub mod target; pub mod values_map; pub mod ven; -mod serde_rfc3339 { +pub mod serde_rfc3339 { use super::*; use chrono::{DateTime, TimeZone, Utc};