From ed958a8ed0817fb49cf4544193b3d692149e3e0e Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Thu, 5 Jun 2025 14:24:48 +0100 Subject: [PATCH] Add sqlx Signed-off-by: Nikolaos Karaolidis --- .env | 1 + .envrc | 2 +- .gitlab-ci.yml | 1 + ...e94011405a313f89943cff7e64e3ccc674822.json | 22 + ...f4cc3de4128ef0aa3383369092f2df56636d9.json | 22 + ...2b6cd170d0692354100d0a1c25c2dba9e9e6b.json | 14 + ...c8b1fe7e32e4b9b501674ea138acf0cd759ff.json | 14 + ...4e624f3e7ae6e2474d03c8619fa1816edefe0.json | 28 + ...ec52a8050d21c453d7ec221be44bd0d893fd1.json | 19 + ...43397316f8d57ac4d9b09248bb5b0f767b166.json | 52 ++ ...edccfe031a21ad1ca8240024adb7e0006570b.json | 15 + ...4ca713d3a3b5556ac26c3cc51ee138f411982.json | 58 ++ ...324c16d76ec2797f68da75fc6e526a3cd0bc4.json | 56 ++ ...e86d87b1e4a6916836ca0d1a0509a159affc8.json | 22 + ...f57258db1212ef4120868581cd0a8a81eff8f.json | 14 + ...ed8b759f946580870558c699cce9490a0e0f2.json | 14 + ...0b93cd67767cf299f443023d8ab9f9a12c44c.json | 19 + ...add43558dc3a99c29246cf61d2431ddf34cf8.json | 26 + ...56f6103ab7caebc26886346e4ecec399bd86c.json | 14 + Cargo.lock | 560 +++++++++++++++++- Cargo.toml | 2 + README.md | 2 +- flake.lock | 84 +++ flake.nix | 52 ++ migrations/20250605080246_init.sql | 56 ++ src/config.rs | 10 + src/main.rs | 13 +- src/models/authelia.rs | 43 +- src/models/groups.rs | 176 ++++-- src/models/intersections.rs | 74 +++ src/models/invites.rs | 4 +- src/models/mod.rs | 1 + src/models/users.rs | 241 ++++++-- src/routes/groups.rs | 252 ++++---- src/routes/users.rs | 271 +++++---- src/state.rs | 30 +- src/utils/authelia.rs | 39 -- src/utils/mod.rs | 1 - support/Containerfile | 11 +- support/manifest.yaml | 13 + treefmt.nix | 17 + 41 files changed, 1885 insertions(+), 480 deletions(-) create mode 100644 .env create mode 100644 .sqlx/query-090673660f991b66b0b5a7e2492e94011405a313f89943cff7e64e3ccc674822.json create mode 100644 .sqlx/query-19d85e2094bcb4ac818975b9477f4cc3de4128ef0aa3383369092f2df56636d9.json create mode 100644 .sqlx/query-275592cdd00626bcb0c5c3054952b6cd170d0692354100d0a1c25c2dba9e9e6b.json create mode 100644 .sqlx/query-282189b1fc3f70e5c2de3f19a3cc8b1fe7e32e4b9b501674ea138acf0cd759ff.json create mode 100644 .sqlx/query-52bcae42b069a7665baeff903774e624f3e7ae6e2474d03c8619fa1816edefe0.json create mode 100644 .sqlx/query-5dbde6bba584448a7be9fd6965aec52a8050d21c453d7ec221be44bd0d893fd1.json create mode 100644 .sqlx/query-74d4ef98ee975bfe90418171dea43397316f8d57ac4d9b09248bb5b0f767b166.json create mode 100644 .sqlx/query-91b332e6af78793ae53cfdbf8e5edccfe031a21ad1ca8240024adb7e0006570b.json create mode 100644 .sqlx/query-9313aac97fa5191c47874e2e3834ca713d3a3b5556ac26c3cc51ee138f411982.json create mode 100644 .sqlx/query-95bbd23a12bf44b1bc31859a1fd324c16d76ec2797f68da75fc6e526a3cd0bc4.json create mode 100644 .sqlx/query-9caa0dac7d2a5098a09278e2331e86d87b1e4a6916836ca0d1a0509a159affc8.json create mode 100644 .sqlx/query-adb2455e26b1cddf90a54d08e79f57258db1212ef4120868581cd0a8a81eff8f.json create mode 100644 .sqlx/query-b1be2a377b5bfaf093618d049c0ed8b759f946580870558c699cce9490a0e0f2.json create mode 100644 .sqlx/query-ba1cb3d9ffd5dd2260815616abc0b93cd67767cf299f443023d8ab9f9a12c44c.json create mode 100644 .sqlx/query-e52660da218cabe80565d95bf77add43558dc3a99c29246cf61d2431ddf34cf8.json create mode 100644 .sqlx/query-e7258b575bc6d1d71f9c62a9c6b56f6103ab7caebc26886346e4ecec399bd86c.json create mode 100644 flake.lock create mode 100755 flake.nix create mode 100644 migrations/20250605080246_init.sql create mode 100644 src/models/intersections.rs delete mode 100644 src/utils/authelia.rs create mode 100755 treefmt.nix diff --git a/.env b/.env new file mode 100644 index 0000000..ec5271a --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=postgresql://glyph:glyph@localhost:5432/glyph diff --git a/.envrc b/.envrc index 481a7b6..3550a30 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -use flake self#rust +use flake diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 538f87a..b068c22 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,6 +16,7 @@ cache: &global_cache variables: RUSTFLAGS: "-Dwarnings" + SQLX_OFFLINE: "true" build: image: registry.karaolidis.com/karaolidis/glyph/rust diff --git a/.sqlx/query-090673660f991b66b0b5a7e2492e94011405a313f89943cff7e64e3ccc674822.json b/.sqlx/query-090673660f991b66b0b5a7e2492e94011405a313f89943cff7e64e3ccc674822.json new file mode 100644 index 0000000..66dc25a --- /dev/null +++ b/.sqlx/query-090673660f991b66b0b5a7e2492e94011405a313f89943cff7e64e3ccc674822.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT COUNT(*) AS \"count!\"\n FROM users\n WHERE name = ANY($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "TextArray" + ] + }, + "nullable": [ + null + ] + }, + "hash": "090673660f991b66b0b5a7e2492e94011405a313f89943cff7e64e3ccc674822" +} diff --git a/.sqlx/query-19d85e2094bcb4ac818975b9477f4cc3de4128ef0aa3383369092f2df56636d9.json b/.sqlx/query-19d85e2094bcb4ac818975b9477f4cc3de4128ef0aa3383369092f2df56636d9.json new file mode 100644 index 0000000..860846c --- /dev/null +++ b/.sqlx/query-19d85e2094bcb4ac818975b9477f4cc3de4128ef0aa3383369092f2df56636d9.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT name\n FROM groups\n WHERE name = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "19d85e2094bcb4ac818975b9477f4cc3de4128ef0aa3383369092f2df56636d9" +} diff --git a/.sqlx/query-275592cdd00626bcb0c5c3054952b6cd170d0692354100d0a1c25c2dba9e9e6b.json b/.sqlx/query-275592cdd00626bcb0c5c3054952b6cd170d0692354100d0a1c25c2dba9e9e6b.json new file mode 100644 index 0000000..b461d48 --- /dev/null +++ b/.sqlx/query-275592cdd00626bcb0c5c3054952b6cd170d0692354100d0a1c25c2dba9e9e6b.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM groups\n WHERE name = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "275592cdd00626bcb0c5c3054952b6cd170d0692354100d0a1c25c2dba9e9e6b" +} diff --git a/.sqlx/query-282189b1fc3f70e5c2de3f19a3cc8b1fe7e32e4b9b501674ea138acf0cd759ff.json b/.sqlx/query-282189b1fc3f70e5c2de3f19a3cc8b1fe7e32e4b9b501674ea138acf0cd759ff.json new file mode 100644 index 0000000..bdfe759 --- /dev/null +++ b/.sqlx/query-282189b1fc3f70e5c2de3f19a3cc8b1fe7e32e4b9b501674ea138acf0cd759ff.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM users\n WHERE name = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "282189b1fc3f70e5c2de3f19a3cc8b1fe7e32e4b9b501674ea138acf0cd759ff" +} diff --git a/.sqlx/query-52bcae42b069a7665baeff903774e624f3e7ae6e2474d03c8619fa1816edefe0.json b/.sqlx/query-52bcae42b069a7665baeff903774e624f3e7ae6e2474d03c8619fa1816edefe0.json new file mode 100644 index 0000000..4f65e4f --- /dev/null +++ b/.sqlx/query-52bcae42b069a7665baeff903774e624f3e7ae6e2474d03c8619fa1816edefe0.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n g.name,\n COALESCE(array_agg(ug.user_name ORDER BY ug.user_name), ARRAY[]::TEXT[]) AS \"users!\"\n FROM groups g\n LEFT JOIN users_groups ug ON g.name = ug.group_name\n WHERE g.name = $1\n GROUP BY g.name\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "users!", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + null + ] + }, + "hash": "52bcae42b069a7665baeff903774e624f3e7ae6e2474d03c8619fa1816edefe0" +} diff --git a/.sqlx/query-5dbde6bba584448a7be9fd6965aec52a8050d21c453d7ec221be44bd0d893fd1.json b/.sqlx/query-5dbde6bba584448a7be9fd6965aec52a8050d21c453d7ec221be44bd0d893fd1.json new file mode 100644 index 0000000..9b88d1a --- /dev/null +++ b/.sqlx/query-5dbde6bba584448a7be9fd6965aec52a8050d21c453d7ec221be44bd0d893fd1.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO users (name, display_name, password, email, disabled, image)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (name) DO UPDATE\n SET display_name = EXCLUDED.display_name,\n password = EXCLUDED.password,\n email = EXCLUDED.email,\n disabled = EXCLUDED.disabled,\n image = EXCLUDED.image\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text", + "Bool", + "Text" + ] + }, + "nullable": [] + }, + "hash": "5dbde6bba584448a7be9fd6965aec52a8050d21c453d7ec221be44bd0d893fd1" +} diff --git a/.sqlx/query-74d4ef98ee975bfe90418171dea43397316f8d57ac4d9b09248bb5b0f767b166.json b/.sqlx/query-74d4ef98ee975bfe90418171dea43397316f8d57ac4d9b09248bb5b0f767b166.json new file mode 100644 index 0000000..7407713 --- /dev/null +++ b/.sqlx/query-74d4ef98ee975bfe90418171dea43397316f8d57ac4d9b09248bb5b0f767b166.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT name, display_name, password, email, disabled, image\n FROM users\n WHERE name = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "display_name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "password", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "disabled", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "image", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true + ] + }, + "hash": "74d4ef98ee975bfe90418171dea43397316f8d57ac4d9b09248bb5b0f767b166" +} diff --git a/.sqlx/query-91b332e6af78793ae53cfdbf8e5edccfe031a21ad1ca8240024adb7e0006570b.json b/.sqlx/query-91b332e6af78793ae53cfdbf8e5edccfe031a21ad1ca8240024adb7e0006570b.json new file mode 100644 index 0000000..64d5fa1 --- /dev/null +++ b/.sqlx/query-91b332e6af78793ae53cfdbf8e5edccfe031a21ad1ca8240024adb7e0006570b.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO users_groups (user_name, group_name)\n SELECT * FROM UNNEST($1::text[], $2::text[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "TextArray", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "91b332e6af78793ae53cfdbf8e5edccfe031a21ad1ca8240024adb7e0006570b" +} diff --git a/.sqlx/query-9313aac97fa5191c47874e2e3834ca713d3a3b5556ac26c3cc51ee138f411982.json b/.sqlx/query-9313aac97fa5191c47874e2e3834ca713d3a3b5556ac26c3cc51ee138f411982.json new file mode 100644 index 0000000..75c1022 --- /dev/null +++ b/.sqlx/query-9313aac97fa5191c47874e2e3834ca713d3a3b5556ac26c3cc51ee138f411982.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n u.name,\n u.display_name,\n u.password,\n u.email,\n u.disabled,\n u.image,\n COALESCE(array_agg(ug.group_name ORDER BY ug.group_name), ARRAY[]::TEXT[]) AS \"groups!\"\n FROM users u\n LEFT JOIN users_groups ug ON u.name = ug.user_name\n WHERE u.name = $1\n GROUP BY u.name, u.email, u.disabled, u.image\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "display_name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "password", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "disabled", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "image", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "groups!", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + null + ] + }, + "hash": "9313aac97fa5191c47874e2e3834ca713d3a3b5556ac26c3cc51ee138f411982" +} diff --git a/.sqlx/query-95bbd23a12bf44b1bc31859a1fd324c16d76ec2797f68da75fc6e526a3cd0bc4.json b/.sqlx/query-95bbd23a12bf44b1bc31859a1fd324c16d76ec2797f68da75fc6e526a3cd0bc4.json new file mode 100644 index 0000000..44f7ab1 --- /dev/null +++ b/.sqlx/query-95bbd23a12bf44b1bc31859a1fd324c16d76ec2797f68da75fc6e526a3cd0bc4.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n u.name,\n u.display_name,\n u.password,\n u.email,\n u.disabled,\n u.image,\n COALESCE(array_agg(ug.group_name ORDER BY ug.group_name), ARRAY[]::TEXT[]) AS \"groups!\"\n FROM users u\n LEFT JOIN users_groups ug ON u.name = ug.user_name\n GROUP BY u.name, u.email, u.disabled, u.image\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "display_name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "password", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "disabled", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "image", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "groups!", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + null + ] + }, + "hash": "95bbd23a12bf44b1bc31859a1fd324c16d76ec2797f68da75fc6e526a3cd0bc4" +} diff --git a/.sqlx/query-9caa0dac7d2a5098a09278e2331e86d87b1e4a6916836ca0d1a0509a159affc8.json b/.sqlx/query-9caa0dac7d2a5098a09278e2331e86d87b1e4a6916836ca0d1a0509a159affc8.json new file mode 100644 index 0000000..662e2f4 --- /dev/null +++ b/.sqlx/query-9caa0dac7d2a5098a09278e2331e86d87b1e4a6916836ca0d1a0509a159affc8.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT COUNT(*) AS \"count!\"\n FROM groups\n WHERE name = ANY($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "TextArray" + ] + }, + "nullable": [ + null + ] + }, + "hash": "9caa0dac7d2a5098a09278e2331e86d87b1e4a6916836ca0d1a0509a159affc8" +} diff --git a/.sqlx/query-adb2455e26b1cddf90a54d08e79f57258db1212ef4120868581cd0a8a81eff8f.json b/.sqlx/query-adb2455e26b1cddf90a54d08e79f57258db1212ef4120868581cd0a8a81eff8f.json new file mode 100644 index 0000000..2cc1724 --- /dev/null +++ b/.sqlx/query-adb2455e26b1cddf90a54d08e79f57258db1212ef4120868581cd0a8a81eff8f.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM users_groups\n WHERE group_name = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "adb2455e26b1cddf90a54d08e79f57258db1212ef4120868581cd0a8a81eff8f" +} diff --git a/.sqlx/query-b1be2a377b5bfaf093618d049c0ed8b759f946580870558c699cce9490a0e0f2.json b/.sqlx/query-b1be2a377b5bfaf093618d049c0ed8b759f946580870558c699cce9490a0e0f2.json new file mode 100644 index 0000000..7c5e5c0 --- /dev/null +++ b/.sqlx/query-b1be2a377b5bfaf093618d049c0ed8b759f946580870558c699cce9490a0e0f2.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO groups (name) VALUES ($1)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "b1be2a377b5bfaf093618d049c0ed8b759f946580870558c699cce9490a0e0f2" +} diff --git a/.sqlx/query-ba1cb3d9ffd5dd2260815616abc0b93cd67767cf299f443023d8ab9f9a12c44c.json b/.sqlx/query-ba1cb3d9ffd5dd2260815616abc0b93cd67767cf299f443023d8ab9f9a12c44c.json new file mode 100644 index 0000000..499e815 --- /dev/null +++ b/.sqlx/query-ba1cb3d9ffd5dd2260815616abc0b93cd67767cf299f443023d8ab9f9a12c44c.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO users (name, display_name, password, email, disabled, image)\n VALUES ($1, $2, $3, $4, $5, $6)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text", + "Bool", + "Text" + ] + }, + "nullable": [] + }, + "hash": "ba1cb3d9ffd5dd2260815616abc0b93cd67767cf299f443023d8ab9f9a12c44c" +} diff --git a/.sqlx/query-e52660da218cabe80565d95bf77add43558dc3a99c29246cf61d2431ddf34cf8.json b/.sqlx/query-e52660da218cabe80565d95bf77add43558dc3a99c29246cf61d2431ddf34cf8.json new file mode 100644 index 0000000..7854eef --- /dev/null +++ b/.sqlx/query-e52660da218cabe80565d95bf77add43558dc3a99c29246cf61d2431ddf34cf8.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n g.name,\n COALESCE(array_agg(ug.user_name ORDER BY ug.user_name), ARRAY[]::TEXT[]) AS \"users!\"\n FROM groups g\n LEFT JOIN users_groups ug ON g.name = ug.group_name\n GROUP BY g.name\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "users!", + "type_info": "TextArray" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + null + ] + }, + "hash": "e52660da218cabe80565d95bf77add43558dc3a99c29246cf61d2431ddf34cf8" +} diff --git a/.sqlx/query-e7258b575bc6d1d71f9c62a9c6b56f6103ab7caebc26886346e4ecec399bd86c.json b/.sqlx/query-e7258b575bc6d1d71f9c62a9c6b56f6103ab7caebc26886346e4ecec399bd86c.json new file mode 100644 index 0000000..91d865c --- /dev/null +++ b/.sqlx/query-e7258b575bc6d1d71f9c62a9c6b56f6103ab7caebc26886346e4ecec399bd86c.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM users_groups\n WHERE user_name = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "e7258b575bc6d1d71f9c62a9c6b56f6103ab7caebc26886346e4ecec399bd86c" +} diff --git a/Cargo.lock b/Cargo.lock index 51e668e..c8aa429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -283,6 +289,15 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -396,7 +411,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -443,6 +458,9 @@ name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] [[package]] name = "blake2" @@ -655,6 +673,30 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -846,6 +888,12 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dtoa" version = "0.4.8" @@ -901,6 +949,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -939,6 +990,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if 1.0.0", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -988,12 +1050,29 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1003,6 +1082,22 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369" +dependencies = [ + "libc", + "log", + "memchr", + "nix", + "page_size", + "pkg-config", + "smallvec", + "zerocopy", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1010,6 +1105,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1018,6 +1114,28 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -1056,8 +1174,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1129,6 +1249,7 @@ dependencies = [ "axum", "axum-extra", "clap", + "fuser", "log", "log4rs", "non-empty-string", @@ -1139,6 +1260,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sqlx", "time", "tokio", "uuid", @@ -1166,6 +1288,20 @@ name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.3", +] [[package]] name = "headers" @@ -1237,6 +1373,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.3.1" @@ -1592,6 +1737,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1670,6 +1825,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if 1.0.0", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1702,6 +1867,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "cfg_aliases", + "libc", +] + [[package]] name = "non-empty-string" version = "0.2.6" @@ -1886,6 +2063,16 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking" version = "2.2.1" @@ -1912,7 +2099,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1994,6 +2181,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "polling" version = "3.8.0" @@ -2690,6 +2883,9 @@ name = "smallvec" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -2706,6 +2902,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -2717,12 +2916,219 @@ dependencies = [ "der", ] +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.4.0", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.3", + "hashlink", + "indexmap 2.9.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.12", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.101", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.101", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "itoa 1.0.15", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1 0.10.6", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.12", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "itoa 1.0.15", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.12", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.12", + "time", + "tracing", + "url", + "uuid", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2921,6 +3327,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.6.10" @@ -3002,9 +3419,21 @@ checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "tracing-core" version = "0.1.33" @@ -3035,12 +3464,33 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unsafe-any-ors" version = "1.0.0" @@ -3103,6 +3553,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -3133,6 +3589,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -3233,6 +3695,16 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3314,13 +3786,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3329,7 +3810,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -3338,28 +3834,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3372,24 +3886,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 894363b..c24b964 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ async-session = "3.0.0" axum = { version = "0.8.4", features = ["macros"] } axum-extra = { version = "0.10.1", features = ["typed-header"] } clap = { version = "4.5.39", features = ["derive"] } +fuser = "0.15.1" log = "0.4.27" log4rs = "1.3.0" non-empty-string = { version = "0.2.6", features = ["serde"] } @@ -30,6 +31,7 @@ redis-macros = "0.5.4" serde = "1.0.219" serde_json = "1.0.140" serde_yaml = "0.9.34" +sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "time", "uuid"] } time = { version = "0.3.41", features = ["serde"] } tokio = { version = "1.45.1", features = ["rt-multi-thread", "process"] } uuid = { version = "1.17.0", features = ["serde"] } diff --git a/README.md b/README.md index 837ac3b..84c6b5c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # glyph -*Glyph* is an Authelia user file database manager. Because files are light but unweildy, and LDAP is convenient but complex. +*Glyph* is an Authelia user file database manager. Because files are light but unwieldy, and LDAP is convenient but complex. ## Development diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..584d841 --- /dev/null +++ b/flake.lock @@ -0,0 +1,84 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "ref": "main", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1748939429, + "narHash": "sha256-IrdLwKWucb9xj1dOpbXHuaV1GzHYx51ZGF4wbl5NPwU=", + "owner": "karaolidis", + "repo": "nixpkgs", + "rev": "7b041169050f5a7b6a15bacdb68a935cee995fe7", + "type": "github" + }, + "original": { + "owner": "karaolidis", + "ref": "integration", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1748243702, + "narHash": "sha256-9YzfeN8CB6SzNPyPm2XjRRqSixDopTapaRsnTpXUEY8=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "1f3f7b784643d488ba4bf315638b2b0a4c5fb007", + "type": "github" + }, + "original": { + "owner": "numtide", + "ref": "main", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100755 index 0000000..24dfb2e --- /dev/null +++ b/flake.nix @@ -0,0 +1,52 @@ +{ + inputs = { + nixpkgs = { + type = "github"; + owner = "karaolidis"; + repo = "nixpkgs"; + ref = "integration"; + }; + + flake-utils = { + type = "github"; + owner = "numtide"; + repo = "flake-utils"; + ref = "main"; + }; + + treefmt-nix = { + type = "github"; + owner = "numtide"; + repo = "treefmt-nix"; + ref = "main"; + + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { self, nixpkgs, ... }@inputs: + inputs.flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + treefmt = inputs.treefmt-nix.lib.evalModule pkgs ./treefmt.nix; + in + { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + cargo + rustc + rustfmt + clippy + cargo-udeps + cargo-outdated + sqlx-cli + ]; + }; + + formatter = treefmt.config.build.wrapper; + checks.formatting = treefmt.config.build.check self; + } + ); +} diff --git a/migrations/20250605080246_init.sql b/migrations/20250605080246_init.sql new file mode 100644 index 0000000..d916a9f --- /dev/null +++ b/migrations/20250605080246_init.sql @@ -0,0 +1,56 @@ +CREATE TABLE IF NOT EXISTS users ( + name TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + password TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + disabled BOOLEAN NOT NULL, + image TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS groups ( + name TEXT PRIMARY KEY, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS users_groups ( + user_name TEXT NOT NULL, + group_name TEXT NOT NULL, + PRIMARY KEY (user_name, group_name), + FOREIGN KEY (user_name) REFERENCES users(name) ON DELETE CASCADE, + FOREIGN KEY (group_name) REFERENCES groups(name) ON DELETE CASCADE +); + +CREATE OR REPLACE FUNCTION update_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_users_timestamp +BEFORE UPDATE ON users +FOR EACH ROW +EXECUTE FUNCTION update_timestamp(); + +CREATE TRIGGER update_groups_timestamp +BEFORE UPDATE ON groups +FOR EACH ROW +EXECUTE FUNCTION update_timestamp(); + +CREATE OR REPLACE FUNCTION update_users_groups_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE users SET updated_at = NOW() WHERE name = NEW.user_name; + UPDATE groups SET updated_at = NOW() WHERE name = NEW.group_name; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_users_groups_timestamp +AFTER INSERT OR DELETE ON users_groups +FOR EACH ROW +EXECUTE FUNCTION update_users_groups_timestamp(); diff --git a/src/config.rs b/src/config.rs index d051dd4..7e961d2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -41,6 +41,15 @@ pub struct AutheliaConfig { pub user_database: PathBuf, } +#[derive(Clone, Deserialize)] +pub struct PostgresqlConfig { + pub user: String, + pub password: String, + pub host: String, + pub port: u16, + pub database: String, +} + #[derive(Clone, Deserialize)] pub struct RedisConfig { pub host: String, @@ -54,6 +63,7 @@ pub struct Config { pub server: ServerConfig, pub oauth: OAuthConfig, pub authelia: AutheliaConfig, + pub postgresql: PostgresqlConfig, pub redis: RedisConfig, } diff --git a/src/main.rs b/src/main.rs index 8ac5747..33460b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use axum::serve; use clap::Parser; use log::info; use log4rs::config::Deserializers; -use std::net::SocketAddr; +use std::{error::Error, net::SocketAddr}; use tokio::net::TcpListener; use config::{Args, Config}; @@ -25,6 +25,8 @@ async fn main() { let config = Config::try_from(&args.config).unwrap(); let state = State::from_config(config.clone()).await; + init(&state).await.unwrap(); + let routes = routes::routes(state); let app = axum::Router::new().nest(&format!("{}/api", config.server.subpath), routes); @@ -34,3 +36,12 @@ async fn main() { info!("Listening on {}", listener.local_addr().unwrap()); serve(listener, app).await.unwrap(); } + +async fn init(state: &State) -> Result<(), Box> { + sqlx::migrate!("./migrations") + .run(&state.pg_pool) + .await + .expect("Failed to run migrations"); + + Ok(()) +} diff --git a/src/models/authelia.rs b/src/models/authelia.rs index a10e002..a96f1f5 100644 --- a/src/models/authelia.rs +++ b/src/models/authelia.rs @@ -1,4 +1,3 @@ -use non_empty_string::NonEmptyString; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -6,50 +5,20 @@ use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UsersFile { - pub users: HashMap, + pub users: HashMap, #[serde(flatten)] - pub extra: Option>, + pub extra: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserFile { - pub displayname: NonEmptyString, - pub password: NonEmptyString, + pub displayname: String, + pub password: String, pub email: Option, pub disabled: Option, - pub groups: Option>, + pub groups: Option>, #[serde(flatten)] - pub extra: Option>, -} - -impl From for UserFile { - fn from(user: super::users::User) -> Self { - Self { - displayname: user.displayname, - email: user.email, - password: user.password, - disabled: if user.disabled { Some(true) } else { None }, - groups: if user.groups.is_empty() { - None - } else { - Some(user.groups) - }, - extra: user.extra, - } - } -} - -impl From for UsersFile { - fn from(users: super::users::Users) -> Self { - Self { - users: users - .users - .into_iter() - .map(|(key, user)| (key, UserFile::from(user))) - .collect(), - extra: users.extra, - } - } + pub extra: Option>, } diff --git a/src/models/groups.rs b/src/models/groups.rs index d7fea95..a88cbe9 100644 --- a/src/models/groups.rs +++ b/src/models/groups.rs @@ -1,49 +1,143 @@ -use non_empty_string::NonEmptyString; -use serde::{Deserialize, Serialize}; +use std::error::Error; -use std::{ - collections::HashMap, - ops::{Deref, DerefMut}, -}; +use serde::{Deserialize, Serialize}; +use sqlx::{PgPool, prelude::FromRow, query, query_as}; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Group { + pub name: String, +} + +impl Group { + pub async fn select_by_name( + pool: &PgPool, + name: &str, + ) -> Result, Box> { + let group = query_as!( + Group, + r#" + SELECT name + FROM groups + WHERE name = $1 + "#, + name + ) + .fetch_optional(pool) + .await?; + + Ok(group) + } + + pub async fn delete_by_name( + pool: &PgPool, + name: &str, + ) -> Result<(), Box> { + query!( + r#" + DELETE FROM groups + WHERE name = $1 + "#, + name + ) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn all_exist_by_names( + pool: &PgPool, + names: &[String], + ) -> Result> { + let row = query!( + r#" + SELECT COUNT(*) AS "count!" + FROM groups + WHERE name = ANY($1) + "#, + names + ) + .fetch_one(pool) + .await?; + + Ok(row.count == i64::try_from(names.len()).unwrap()) + } +} #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Group { - pub users: Vec, +pub struct GroupWithUsers { + pub name: String, + pub users: Vec, } -pub struct Groups { - pub groups: HashMap, -} - -impl Deref for Groups { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.groups - } -} - -impl DerefMut for Groups { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.groups - } -} - -impl From for Groups { - fn from(users_file: super::authelia::UsersFile) -> Self { - users_file.users.into_iter().fold( - Self { - groups: HashMap::new(), - }, - |mut acc, (key, user)| { - for group in user.groups.unwrap_or_default() { - acc.entry(group) - .or_insert_with(|| Group { users: Vec::new() }) - .users - .push(key.clone()); - } - acc - }, +impl GroupWithUsers { + pub async fn select(pool: &PgPool) -> Result, Box> { + let groups = query_as!( + GroupWithUsers, + r#" + SELECT + g.name, + COALESCE(array_agg(ug.user_name ORDER BY ug.user_name), ARRAY[]::TEXT[]) AS "users!" + FROM groups g + LEFT JOIN users_groups ug ON g.name = ug.group_name + GROUP BY g.name + "# ) + .fetch_all(pool) + .await?; + + Ok(groups) + } + + pub async fn select_by_name( + pool: &PgPool, + name: &str, + ) -> Result, Box> { + let group = query_as!( + GroupWithUsers, + r#" + SELECT + g.name, + COALESCE(array_agg(ug.user_name ORDER BY ug.user_name), ARRAY[]::TEXT[]) AS "users!" + FROM groups g + LEFT JOIN users_groups ug ON g.name = ug.group_name + WHERE g.name = $1 + GROUP BY g.name + "#, + name + ) + .fetch_optional(pool) + .await?; + + Ok(group) + } + + pub async fn insert( + pool: &PgPool, + group_with_users: &Self, + ) -> Result<(), Box> { + let mut tx = pool.begin().await?; + + query!( + r#"INSERT INTO groups (name) VALUES ($1)"#, + group_with_users.name + ) + .execute(&mut *tx) + .await?; + + query!( + r#" + INSERT INTO users_groups (user_name, group_name) + SELECT * FROM UNNEST($1::text[], $2::text[]) + "#, + &group_with_users.users, + &vec![group_with_users.name.clone(); group_with_users.users.len()] + ) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(()) } } diff --git a/src/models/intersections.rs b/src/models/intersections.rs new file mode 100644 index 0000000..3bfd64a --- /dev/null +++ b/src/models/intersections.rs @@ -0,0 +1,74 @@ +use std::error::Error; + +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, PgPool, query}; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct UsersGroups { + pub user_name: String, + pub group_name: String, +} + +impl UsersGroups { + pub async fn set_users_for_group( + pool: &PgPool, + group_name: &str, + users: &[String], + ) -> Result<(), Box> { + let mut tx = pool.begin().await?; + + query!( + r#" + DELETE FROM users_groups + WHERE group_name = $1 + "#, + group_name + ) + .execute(&mut *tx) + .await?; + + query!( + r#" + INSERT INTO users_groups (user_name, group_name) + SELECT * FROM UNNEST($1::text[], $2::text[]) + "#, + users, + &vec![group_name.to_string(); users.len()] + ) + .execute(&mut *tx) + .await?; + + Ok(()) + } + + pub async fn set_groups_for_user( + pool: &PgPool, + user_name: &str, + groups: &[String], + ) -> Result<(), Box> { + let mut tx = pool.begin().await?; + + query!( + r#" + DELETE FROM users_groups + WHERE user_name = $1 + "#, + user_name + ) + .execute(&mut *tx) + .await?; + + query!( + r#" + INSERT INTO users_groups (user_name, group_name) + SELECT * FROM UNNEST($1::text[], $2::text[]) + "#, + &vec![user_name.to_string(); groups.len()], + groups + ) + .execute(&mut *tx) + .await?; + + Ok(()) + } +} diff --git a/src/models/invites.rs b/src/models/invites.rs index 7886e56..15b77b5 100644 --- a/src/models/invites.rs +++ b/src/models/invites.rs @@ -1,9 +1,9 @@ -use redis_macros::{FromRedisValue, ToRedisArgs}; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use time::UtcDateTime; use uuid::Uuid; -#[derive(Serialize, Deserialize, FromRedisValue, ToRedisArgs)] +#[derive(Serialize, Deserialize, FromRow)] struct Invite { id: Uuid, groups: Vec, diff --git a/src/models/mod.rs b/src/models/mod.rs index 2071bc5..c620df4 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod authelia; pub mod groups; +pub mod intersections; pub mod invites; pub mod users; diff --git a/src/models/users.rs b/src/models/users.rs index 8fac784..9ebdfac 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -1,68 +1,199 @@ -use non_empty_string::NonEmptyString; +use std::error::Error; + use serde::{Deserialize, Serialize}; -use serde_json::Value; +use sqlx::{FromRow, PgPool, query, query_as}; -use std::{ - collections::HashMap, - ops::{Deref, DerefMut}, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct User { - pub displayname: NonEmptyString, - pub email: Option, - pub password: NonEmptyString, + pub name: String, + pub display_name: String, + pub password: String, + pub email: String, + #[serde(default)] pub disabled: bool, - pub groups: Vec, + #[serde(default)] + pub image: Option, +} - #[serde(flatten)] - pub extra: Option>, +impl User { + pub async fn select_by_name( + pool: &PgPool, + name: &str, + ) -> Result, Box> { + let user = query_as!( + User, + r#" + SELECT name, display_name, password, email, disabled, image + FROM users + WHERE name = $1 + "#, + name + ) + .fetch_optional(pool) + .await?; + + Ok(user) + } + + pub async fn upsert(pool: &PgPool, user: &Self) -> Result<(), Box> { + query!( + r#" + INSERT INTO users (name, display_name, password, email, disabled, image) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (name) DO UPDATE + SET display_name = EXCLUDED.display_name, + password = EXCLUDED.password, + email = EXCLUDED.email, + disabled = EXCLUDED.disabled, + image = EXCLUDED.image + "#, + user.name, + user.display_name, + user.password, + user.email, + user.disabled, + user.image + ) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn delete_by_name( + pool: &PgPool, + name: &str, + ) -> Result<(), Box> { + query!( + r#" + DELETE FROM users + WHERE name = $1 + "#, + name + ) + .execute(pool) + .await?; + + Ok(()) + } + + pub async fn all_exist_by_names( + pool: &PgPool, + names: &[String], + ) -> Result> { + let row = query!( + r#" + SELECT COUNT(*) AS "count!" + FROM users + WHERE name = ANY($1) + "#, + names + ) + .fetch_one(pool) + .await?; + + Ok(row.count == i64::try_from(names.len()).unwrap()) + } } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Users { - pub users: HashMap, - - #[serde(flatten)] - pub extra: Option>, +pub struct UserWithGroups { + pub name: String, + pub display_name: String, + pub password: String, + pub email: String, + #[serde(default)] + pub disabled: bool, + #[serde(default)] + pub image: Option, + pub groups: Vec, } -impl Deref for Users { - type Target = HashMap; +impl UserWithGroups { + pub async fn select(pool: &PgPool) -> Result, Box> { + let users = query_as!( + UserWithGroups, + r#" + SELECT + u.name, + u.display_name, + u.password, + u.email, + u.disabled, + u.image, + COALESCE(array_agg(ug.group_name ORDER BY ug.group_name), ARRAY[]::TEXT[]) AS "groups!" + FROM users u + LEFT JOIN users_groups ug ON u.name = ug.user_name + GROUP BY u.name, u.email, u.disabled, u.image + "# + ) + .fetch_all(pool) + .await?; - fn deref(&self) -> &Self::Target { - &self.users - } -} - -impl DerefMut for Users { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.users - } -} - -impl From for User { - fn from(user_file: super::authelia::UserFile) -> Self { - Self { - displayname: user_file.displayname, - email: user_file.email, - password: user_file.password, - disabled: user_file.disabled.unwrap_or(false), - groups: user_file.groups.unwrap_or_default(), - extra: user_file.extra, - } - } -} - -impl From for Users { - fn from(users_file: super::authelia::UsersFile) -> Self { - Self { - users: users_file - .users - .into_iter() - .map(|(key, user)| (key, User::from(user))) - .collect(), - extra: users_file.extra, - } + Ok(users) + } + + pub async fn select_by_name( + pool: &PgPool, + name: &str, + ) -> Result, Box> { + let user = query_as!( + UserWithGroups, + r#" + SELECT + u.name, + u.display_name, + u.password, + u.email, + u.disabled, + u.image, + COALESCE(array_agg(ug.group_name ORDER BY ug.group_name), ARRAY[]::TEXT[]) AS "groups!" + FROM users u + LEFT JOIN users_groups ug ON u.name = ug.user_name + WHERE u.name = $1 + GROUP BY u.name, u.email, u.disabled, u.image + "#, + name + ) + .fetch_optional(pool) + .await?; + + Ok(user) + } + + pub async fn insert( + pool: &PgPool, + user_with_groups: &Self, + ) -> Result<(), Box> { + let mut tx = pool.begin().await?; + + query!( + r#"INSERT INTO users (name, display_name, password, email, disabled, image) + VALUES ($1, $2, $3, $4, $5, $6) + "#, + user_with_groups.name, + user_with_groups.display_name, + user_with_groups.password, + user_with_groups.email, + user_with_groups.disabled, + user_with_groups.image + ) + .execute(&mut *tx) + .await?; + + query!( + r#" + INSERT INTO users_groups (user_name, group_name) + SELECT * FROM UNNEST($1::text[], $2::text[]) + "#, + &user_with_groups.groups, + &vec![user_with_groups.name.clone(); user_with_groups.groups.len()] + ) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(()) } } diff --git a/src/routes/groups.rs b/src/routes/groups.rs index 6fa0a5e..80b653a 100644 --- a/src/routes/groups.rs +++ b/src/routes/groups.rs @@ -6,227 +6,173 @@ use axum::{ response::{IntoResponse, Redirect}, routing, }; -use log::error; use non_empty_string::NonEmptyString; use serde::{Deserialize, Serialize}; +use sqlx::PgPool; -use crate::{models::groups, routes::auth, state::State}; +use crate::{ + config::Config, + models::{self, groups::Group}, + routes::auth, + state::State, +}; #[derive(Debug, Serialize)] struct GroupResponse { - groupname: NonEmptyString, - users: Vec, + users: Vec, } -impl From<(NonEmptyString, groups::Group)> for GroupResponse { - fn from((groupname, group): (NonEmptyString, groups::Group)) -> Self { - Self { - groupname, - users: group.users, - } +impl From for GroupResponse { + fn from(group: models::groups::GroupWithUsers) -> Self { + Self { users: group.users } } } -type GroupsResponse = HashMap; - -impl From for GroupsResponse { - fn from(groups: groups::Groups) -> Self { - groups - .groups - .into_iter() - .map(|(key, group)| (key.clone(), GroupResponse::from((key, group)))) - .collect() - } -} +type GroupsResponse = HashMap; pub async fn get_all( - _user: auth::User, - extract::State(state): extract::State, + _: auth::User, + extract::State(pg_pool): extract::State, ) -> Result { - let groups = state.load_groups().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let groups_with_users = models::groups::GroupWithUsers::select(&pg_pool) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; - Ok(Json(GroupsResponse::from(groups))) + let groups_response = groups_with_users + .into_iter() + .map(|group| (group.name.clone(), GroupResponse::from(group))) + .collect::(); + + Ok(Json(groups_response)) } pub async fn get( - _user: auth::User, - extract::Path(groupname): extract::Path, - extract::State(state): extract::State, + _: auth::User, + extract::Path(name): extract::Path, + extract::State(pg_pool): extract::State, ) -> Result { - let groups = state.load_groups().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let group_with_users = models::groups::GroupWithUsers::select_by_name(&pg_pool, name.as_str()) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + .ok_or(StatusCode::NOT_FOUND)?; - groups.get(&groupname).cloned().map_or_else( - || Err(StatusCode::NOT_FOUND), - |group| Ok(Json(GroupResponse::from((groupname, group))).into_response()), - ) + Ok(Json(GroupResponse::from(group_with_users))) } #[derive(Debug, Deserialize)] pub struct GroupCreate { - groupname: NonEmptyString, + name: NonEmptyString, users: Vec, } -impl From for groups::Group { - fn from(update: GroupCreate) -> Self { - Self { - users: update.users, - } - } -} - pub async fn create( - _user: auth::User, - extract::State(state): extract::State, + _: auth::User, + extract::State(pg_pool): extract::State, extract::Json(group_create): extract::Json, ) -> Result { - let (mut users, groups) = state.load_users_and_groups().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - let groupname = group_create.groupname.clone(); - if groups.contains_key(&groupname) { + if models::groups::Group::select_by_name(&pg_pool, group_create.name.as_str()) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + .is_some() + { return Err(StatusCode::CONFLICT); } - let group_created = groups::Group::from(group_create); + let users = group_create + .users + .into_iter() + .map(|u| u.to_string()) + .collect::>(); - for username in &group_created.users { - if !users.contains_key(username) { - return Err(StatusCode::NOT_FOUND); - } - - users - .get_mut(username) - .unwrap() - .groups - .push(groupname.clone()); + if !models::users::User::all_exist_by_names(&pg_pool, &users) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + { + return Err(StatusCode::NOT_FOUND); } - state.save_users(users).map_err(|e| { - error!("Failed to save users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let group_with_users = models::groups::GroupWithUsers { + name: group_create.name.to_string(), + users, + }; - Ok(Json(GroupResponse::from((groupname, group_created))).into_response()) + models::groups::GroupWithUsers::insert(&pg_pool, &group_with_users) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(()) } #[derive(Debug, Deserialize)] pub struct GroupUpdate { - groupname: Option, - users: Vec, -} - -impl From for groups::Group { - fn from(update: GroupUpdate) -> Self { - Self { - users: update.users, - } - } + users: Option>, } pub async fn update( - user: auth::User, - extract::Path(groupname): extract::Path, - extract::State(state): extract::State, + session_user: auth::User, + extract::Path(name): extract::Path, + extract::State(pg_pool): extract::State, + extract::State(config): extract::State, extract::Json(group_update): extract::Json, ) -> Result { - let (mut users, groups) = state.load_users_and_groups().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let group = models::groups::Group::select_by_name(&pg_pool, name.as_str()) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + .ok_or(StatusCode::NOT_FOUND)?; - let new_groupname = group_update - .groupname - .clone() - .unwrap_or_else(|| groupname.clone()); + let mut logout = false; - let group_existing = groups.get(&groupname).ok_or(StatusCode::NOT_FOUND)?; - let group_updated = groups::Group::from(group_update); + if let Some(users) = &group_update.users { + let users = users.iter().map(ToString::to_string).collect::>(); - if groupname != new_groupname - && (groupname == state.config.oauth.admin_group - || new_groupname == state.config.oauth.admin_group) - { - return Err(StatusCode::FORBIDDEN); - } - - if groupname != new_groupname && groups.contains_key(&new_groupname) { - return Err(StatusCode::CONFLICT); - } - - for user in &group_existing.users { - let user = users.get_mut(user).unwrap(); - let pos = user.groups.iter().position(|g| g == &groupname).unwrap(); - user.groups.remove(pos); - } - - for username in &group_updated.users { - if !users.contains_key(username) { + if !models::users::User::all_exist_by_names(&pg_pool, &users) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + { return Err(StatusCode::NOT_FOUND); } - let user = users.get_mut(username).unwrap(); - if !user.groups.contains(&new_groupname) { - user.groups.push(new_groupname.clone()); + models::intersections::UsersGroups::set_users_for_group( + &pg_pool, + group.name.as_str(), + &users, + ) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; + + if name == config.oauth.admin_group && !users.contains(&session_user.username) { + logout = true; } } - state.save_users(users).map_err(|e| { - error!("Failed to save users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - if new_groupname == state.config.oauth.admin_group - && !group_updated - .users - .iter() - .any(|group_user| *group_user == *user.username.to_string()) - { + if logout { return Ok(Redirect::to("/api/auth/logout").into_response()); } - Ok(Json(GroupResponse::from((new_groupname, group_updated))).into_response()) + Ok(().into_response()) } pub async fn delete( - _user: auth::User, - extract::Path(groupname): extract::Path, - extract::State(state): extract::State, + _: auth::User, + extract::Path(name): extract::Path, + extract::State(pg_pool): extract::State, + extract::State(config): extract::State, ) -> Result { - let (mut users, groups) = state.load_users_and_groups().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - if groupname == state.config.oauth.admin_group { + if name == config.oauth.admin_group { return Err(StatusCode::FORBIDDEN); } - if let Some(old_group) = groups.get(&groupname) { - for user in &old_group.users { - let user = users.get_mut(user).unwrap(); - let pos = user.groups.iter().position(|g| g == &groupname).unwrap(); - user.groups.remove(pos); - } - } else { - return Err(StatusCode::NOT_FOUND); - } + let group = models::groups::Group::select_by_name(&pg_pool, &name) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + .ok_or(StatusCode::NOT_FOUND)?; - state.save_users(users).map_err(|e| { - error!("Failed to save users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + Group::delete_by_name(&pg_pool, &group.name) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; - Ok(StatusCode::NO_CONTENT.into_response()) + Ok(()) } pub fn routes(state: State) -> Router { diff --git a/src/routes/users.rs b/src/routes/users.rs index 67ebdbb..f7bc4af 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -6,205 +6,216 @@ use axum::{ response::{IntoResponse, Redirect}, routing, }; -use log::error; use non_empty_string::NonEmptyString; use serde::{Deserialize, Serialize}; +use sqlx::PgPool; use crate::{ - models::users, routes::auth, state::State, utils::crypto::generate_random_password_hash, + config::Config, models, routes::auth, state::State, + utils::crypto::generate_random_password_hash, }; #[derive(Debug, Serialize)] struct UserResponse { - username: NonEmptyString, - displayname: NonEmptyString, - email: Option, - groups: Option>, + display_name: String, + email: String, + disabled: bool, + image: Option, + groups: Vec, } -impl From<(NonEmptyString, users::User)> for UserResponse { - fn from((username, user): (NonEmptyString, users::User)) -> Self { +impl From for UserResponse { + fn from(user: models::users::UserWithGroups) -> Self { Self { - username, - displayname: user.displayname, + display_name: user.display_name, email: user.email, - groups: Some(user.groups), + disabled: user.disabled, + image: user.image, + groups: user.groups, } } } -type UsersResponse = HashMap; - -impl From for UsersResponse { - fn from(users: users::Users) -> Self { - users - .users - .into_iter() - .map(|(key, user)| (key.clone(), UserResponse::from((key, user)))) - .collect() - } -} +type UsersResponse = HashMap; pub async fn get_all( - _user: auth::User, - extract::State(state): extract::State, + _: auth::User, + extract::State(pg_pool): extract::State, ) -> Result { - let users = state.load_users().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let users_with_groups = models::users::UserWithGroups::select(&pg_pool) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; - Ok(Json(UsersResponse::from(users))) + let users_response = users_with_groups + .into_iter() + .map(|user| (user.name.clone(), UserResponse::from(user))) + .collect::(); + + Ok(Json(users_response)) } pub async fn get( - _user: auth::User, - extract::Path(username): extract::Path, - extract::State(state): extract::State, + _: auth::User, + extract::Path(name): extract::Path, + extract::State(pg_pool): extract::State, ) -> Result { - let users = state.load_users().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let user_with_groups = models::users::UserWithGroups::select_by_name(&pg_pool, name.as_str()) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + .ok_or(StatusCode::NOT_FOUND)?; - users.get(&username).cloned().map_or_else( - || Err(StatusCode::NOT_FOUND), - |user| Ok(Json(UserResponse::from((username, user))).into_response()), - ) + Ok(Json(UserResponse::from(user_with_groups))) } #[derive(Debug, Deserialize)] pub struct UserCreate { - username: NonEmptyString, + name: NonEmptyString, displayname: NonEmptyString, email: NonEmptyString, - disabled: Option, - groups: Option>, -} - -#[allow(clippy::fallible_impl_from)] -impl From for users::User { - fn from(user_create: UserCreate) -> Self { - Self { - displayname: user_create.displayname, - email: Some(user_create.email.to_string()), - password: NonEmptyString::new(generate_random_password_hash()).unwrap(), - disabled: user_create.disabled.unwrap_or(false), - groups: user_create.groups.unwrap_or_default(), - extra: None, - } - } + disabled: bool, + image: Option, + groups: Vec, } pub async fn create( - _user: auth::User, - extract::State(state): extract::State, + _: auth::User, + extract::State(pg_pool): extract::State, extract::Json(user_create): extract::Json, ) -> Result { - let mut users = state.load_users().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - let username = user_create.username.clone(); - if users.contains_key(&username) { + if models::users::User::select_by_name(&pg_pool, user_create.name.as_str()) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + .is_some() + { return Err(StatusCode::CONFLICT); } - let user_created = users::User::from(user_create); - users.users.insert(username.clone(), user_created.clone()); + let groups = user_create + .groups + .into_iter() + .map(|g| g.to_string()) + .collect::>(); - state.save_users(users).map_err(|e| { - error!("Failed to save users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + if !models::groups::Group::all_exist_by_names(&pg_pool, &groups) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + { + return Err(StatusCode::NOT_FOUND); + } - Ok(Json(UserResponse::from((username, user_created))).into_response()) + let user_with_groups = models::users::UserWithGroups { + name: user_create.name.to_string(), + display_name: user_create.displayname.to_string(), + password: generate_random_password_hash(), + email: user_create.email.to_string(), + disabled: user_create.disabled, + image: user_create.image.map(|i| i.to_string()), + groups, + }; + + models::users::UserWithGroups::insert(&pg_pool, &user_with_groups) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(()) } #[derive(Debug, Deserialize)] pub struct UserUpdate { - username: Option, - displayname: NonEmptyString, - email: NonEmptyString, + display_name: Option, + email: Option, disabled: Option, + image: Option, groups: Option>, } -impl From<(Self, UserUpdate)> for users::User { - fn from((user_existing, user_update): (Self, UserUpdate)) -> Self { - Self { - displayname: user_update.displayname, - email: Some(user_update.email.to_string()), - password: user_existing.password, - disabled: user_update.disabled.unwrap_or(user_existing.disabled), - groups: user_update.groups.unwrap_or(user_existing.groups), - extra: user_existing.extra, - } - } -} - pub async fn update( - user: auth::User, - extract::Path(username): extract::Path, - extract::State(state): extract::State, + session_user: auth::User, + extract::Path(name): extract::Path, + extract::State(pg_pool): extract::State, + extract::State(config): extract::State, extract::Json(user_update): extract::Json, ) -> Result { - let mut users = state.load_users().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let user = models::users::User::select_by_name(&pg_pool, name.as_str()) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + .ok_or(StatusCode::NOT_FOUND)?; - let new_username = user_update - .username - .clone() - .unwrap_or_else(|| username.clone()); + let mut logout = false; - let user_existing = users.remove(&username).ok_or(StatusCode::NOT_FOUND)?; - let user_updated = users::User::from((user_existing, user_update)); + if let Some(groups) = user_update.groups { + let groups = groups + .into_iter() + .map(|g| g.to_string()) + .collect::>(); - users - .users - .insert(new_username.clone(), user_updated.clone()); + if !models::groups::Group::all_exist_by_names(&pg_pool, &groups) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + { + return Err(StatusCode::NOT_FOUND); + } - state.save_users(users).map_err(|e| { - error!("Failed to save users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + models::intersections::UsersGroups::set_groups_for_user( + &pg_pool, + user.name.as_str(), + &groups, + ) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; - if user.username.to_string() == username && (username != new_username || user_updated.disabled) - { + if name == session_user.username.to_string() && !groups.contains(&config.oauth.admin_group) + { + logout = true; + } + } + + let user = models::users::User { + name: user.name, + display_name: user_update + .display_name + .map(|d| d.to_string()) + .unwrap_or(user.display_name), + password: user.password, + email: user_update + .email + .map(|e| e.to_string()) + .unwrap_or(user.email), + disabled: user_update.disabled.unwrap_or(user.disabled), + image: user_update.image.map(|i| i.to_string()).or(user.image), + }; + + models::users::User::upsert(&pg_pool, &user) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; + + if logout { return Ok(Redirect::to("/api/auth/logout").into_response()); } - Ok(Json(UserResponse::from((new_username, user_updated))).into_response()) + Ok(().into_response()) } pub async fn delete( - user: auth::User, - extract::Path(username): extract::Path, - extract::State(state): extract::State, + session_user: auth::User, + extract::Path(name): extract::Path, + extract::State(pg_pool): extract::State, ) -> Result { - let mut users = state.load_users().map_err(|e| { - error!("Failed to read users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - if users.remove(&username).is_none() { - return Err(StatusCode::NOT_FOUND); + if name == session_user.username.to_string() { + return Err(StatusCode::FORBIDDEN); } - state.save_users(users).map_err(|e| { - error!("Failed to save users file: {e}"); - StatusCode::INTERNAL_SERVER_ERROR - })?; + let user = models::users::User::select_by_name(&pg_pool, &name) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))? + .ok_or(StatusCode::NOT_FOUND)?; - if user.username.to_string() == username { - return Ok(Redirect::to("/api/auth/logout").into_response()); - } + models::users::User::delete_by_name(&pg_pool, &user.name) + .await + .or(Err(StatusCode::INTERNAL_SERVER_ERROR))?; - Ok(StatusCode::NO_CONTENT.into_response()) + Ok(()) } pub fn routes(state: State) -> Router { diff --git a/src/state.rs b/src/state.rs index 6bac1a1..4d13bca 100644 --- a/src/state.rs +++ b/src/state.rs @@ -11,6 +11,7 @@ use openidconnect::{ reqwest, }; use redis::{self, AsyncCommands}; +use sqlx::{PgPool, postgres::PgPoolOptions}; use tokio::spawn; use crate::config::Config; @@ -47,6 +48,7 @@ pub struct State { pub config: Config, pub oauth_http_client: reqwest::Client, pub oauth_client: OAuthClient, + pub pg_pool: PgPool, pub redis_client: redis::aio::MultiplexedConnection, pub session_store: RedisSessionStore, } @@ -54,6 +56,7 @@ pub struct State { impl State { pub async fn from_config(config: Config) -> Self { let (oauth_http_client, oauth_client) = oauth_client(&config).await; + let pg_pool = pg_pool(&config).await; let redis_client = redis_client(&config).await; let session_store = session_store(&config); @@ -61,6 +64,7 @@ impl State { config, oauth_http_client, oauth_client, + pg_pool, redis_client, session_store, } @@ -85,6 +89,12 @@ impl FromRef for OAuthClient { } } +impl FromRef for PgPool { + fn from_ref(state: &State) -> Self { + state.pg_pool.clone() + } +} + impl FromRef for redis::aio::MultiplexedConnection { fn from_ref(state: &State) -> Self { state.redis_client.clone() @@ -127,6 +137,21 @@ async fn oauth_client(config: &Config) -> (reqwest::Client, OAuthClient) { (oauth_http_client, oauth_client) } +async fn pg_pool(config: &Config) -> PgPool { + PgPoolOptions::new() + .max_connections(5) + .connect(&format!( + "postgres://{}:{}@{}:{}/{}", + config.postgresql.user, + config.postgresql.password, + config.postgresql.host, + config.postgresql.port, + config.postgresql.database + )) + .await + .unwrap() +} + async fn redis_client(config: &Config) -> redis::aio::MultiplexedConnection { let url = format!( "redis://{}:{}/{}", @@ -153,7 +178,7 @@ async fn redis_client(config: &Config) -> redis::aio::MultiplexedConnection { .await .unwrap(); - let channel = format!("__keyevent@{}__:expired", database); + let channel = format!("__keyevent@{database}__:expired"); connection.subscribe(&[channel]).await.unwrap(); while let Some(msg) = rx.recv().await { @@ -178,7 +203,6 @@ fn session_store(config: &Config) -> RedisSessionStore { "redis://{}:{}/{}", config.redis.host, config.redis.port, config.redis.database ); - let session_store = RedisSessionStore::new(url).unwrap().with_prefix("session:"); - session_store + RedisSessionStore::new(url).unwrap().with_prefix("session:") } diff --git a/src/utils/authelia.rs b/src/utils/authelia.rs deleted file mode 100644 index ade7bac..0000000 --- a/src/utils/authelia.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::error::Error; - -use crate::{models, state::State}; - -impl State { - pub fn load_users(&self) -> Result> { - let file_contents = std::fs::read_to_string(&self.config.authelia.user_database)?; - let users_file: models::authelia::UsersFile = serde_yaml::from_str(&file_contents)?; - let users = models::users::Users::from(users_file); - Ok(users) - } - - pub fn load_groups(&self) -> Result> { - let file_contents = std::fs::read_to_string(&self.config.authelia.user_database)?; - let users_file = serde_yaml::from_str::(&file_contents)?; - let groups = models::groups::Groups::from(users_file); - Ok(groups) - } - - pub fn load_users_and_groups( - &self, - ) -> Result<(models::users::Users, models::groups::Groups), Box> { - let file_contents = std::fs::read_to_string(&self.config.authelia.user_database)?; - let users_file = serde_yaml::from_str::(&file_contents)?; - let users = models::users::Users::from(users_file.clone()); - let groups = models::groups::Groups::from(users_file); - Ok((users, groups)) - } - - pub fn save_users( - &self, - users: models::users::Users, - ) -> Result<(), Box> { - let users_file = models::authelia::UsersFile::from(users); - let file_contents = serde_yaml::to_string(&users_file)?; - std::fs::write(&self.config.authelia.user_database, file_contents)?; - Ok(()) - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index b42b1ba..274f0ed 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1 @@ -pub mod authelia; pub mod crypto; diff --git a/support/Containerfile b/support/Containerfile index d410f4b..ad9c78d 100644 --- a/support/Containerfile +++ b/support/Containerfile @@ -2,8 +2,7 @@ FROM docker.io/library/rust AS builder ARG BUILD_MODE=debug -RUN apt-get update && apt-get install -y musl-tools && apt-get clean -RUN rustup target add x86_64-unknown-linux-musl +RUN apt-get update && apt-get clean WORKDIR /app @@ -13,11 +12,13 @@ RUN cargo fetch RUN rm -rf src COPY src ./src +COPY migrations ./migrations +COPY .sqlx ./.sqlx -RUN cargo build --target=x86_64-unknown-linux-musl $(if [ "$BUILD_MODE" = "release" ]; then echo "--release"; else echo ""; fi) -RUN mkdir -p build && cp target/x86_64-unknown-linux-musl/$(if [ "$BUILD_MODE" = "release" ]; then echo "release"; else echo "debug"; fi)/glyph build/glyph +RUN cargo build $(if [ "$BUILD_MODE" = "release" ]; then echo "--release"; else echo ""; fi) +RUN mkdir -p build && cp target/$(if [ "$BUILD_MODE" = "release" ]; then echo "release"; else echo "debug"; fi)/glyph build/glyph -FROM docker.io/library/alpine +FROM docker.io/library/debian:bookworm-slim COPY --from=builder /app/build/glyph /usr/local/bin/glyph diff --git a/support/manifest.yaml b/support/manifest.yaml index 44d651b..0842778 100644 --- a/support/manifest.yaml +++ b/support/manifest.yaml @@ -20,6 +20,19 @@ spec: "/etc/glyph/log4rs.yml", ] + - name: postgresql + image: docker.io/library/postgres:latest + env: + - name: POSTGRES_DB + value: glyph + - name: POSTGRES_USER + value: glyph + - name: POSTGRES_PASSWORD + value: glyph + ports: + - containerPort: 5432 + hostPort: 5432 + - name: redis image: docker.io/library/redis:latest diff --git a/treefmt.nix b/treefmt.nix new file mode 100755 index 0000000..5a60284 --- /dev/null +++ b/treefmt.nix @@ -0,0 +1,17 @@ +{ ... }: +{ + projectRootFile = "flake.nix"; + + programs = { + nixfmt = { + enable = true; + strict = true; + }; + }; + + settings = { + global = { + excludes = [ ".envrc" ]; + }; + }; +}