diff --git a/.sqlx/query-006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb.json b/.sqlx/query-006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb.json deleted file mode 100644 index 2df5961..0000000 --- a/.sqlx/query-006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM settings_blocks WHERE id = $1 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb" -} diff --git a/.sqlx/query-063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527.json b/.sqlx/query-063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527.json deleted file mode 100644 index fdf8aba..0000000 --- a/.sqlx/query-063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM data_points WHERE chart_block_id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527" -} diff --git a/.sqlx/query-08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681.json b/.sqlx/query-08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681.json deleted file mode 100644 index 8adf917..0000000 --- a/.sqlx/query-08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO table_columns (table_block_id, name, label, data_type, is_sortable, is_filterable, width, \"order\")\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Varchar", - "Varchar", - "Varchar", - "Bool", - "Bool", - "Int4", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681" -} diff --git a/.sqlx/query-0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263.json b/.sqlx/query-0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263.json deleted file mode 100644 index 0c3e9b6..0000000 --- a/.sqlx/query-0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, chart_block_id, x, y, label, color\n FROM data_points WHERE chart_block_id = $1 ORDER BY x\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "chart_block_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "x", - "type_info": "Float8" - }, - { - "ordinal": 3, - "name": "y", - "type_info": "Float8" - }, - { - "ordinal": 4, - "name": "label", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "color", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - true - ] - }, - "hash": "0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263" -} diff --git a/.sqlx/query-0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7.json b/.sqlx/query-0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7.json deleted file mode 100644 index c2d117b..0000000 --- a/.sqlx/query-0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \n created_by, updated_by\n FROM pages WHERE slug LIKE '%-settings' AND is_active = true\n ORDER BY title\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "created_by", - "type_info": "Uuid" - }, - { - "ordinal": 8, - "name": "updated_by", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - true, - true - ] - }, - "hash": "0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7" -} diff --git a/.sqlx/query-12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9.json b/.sqlx/query-12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9.json deleted file mode 100644 index 7cb7893..0000000 --- a/.sqlx/query-12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active, created_at as \"created_at: DateTime\", updated_at as \"updated_at: DateTime\"\n FROM hero_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "subtitle", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "background_image", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "background_color", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "text_color", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "cta_text", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "cta_link", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 11, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 12, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false - ] - }, - "hash": "12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9" -} diff --git a/.sqlx/query-1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5.json b/.sqlx/query-1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5.json deleted file mode 100644 index c5bf89e..0000000 --- a/.sqlx/query-1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM hero_blocks WHERE id = $1 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5" -} diff --git a/.sqlx/query-1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c.json b/.sqlx/query-1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c.json deleted file mode 100644 index 3268697..0000000 --- a/.sqlx/query-1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) as count FROM text_blocks WHERE page_id = $1 AND is_active = true", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - null - ] - }, - "hash": "1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c" -} diff --git a/.sqlx/query-20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6.json b/.sqlx/query-20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6.json deleted file mode 100644 index 502aac8..0000000 --- a/.sqlx/query-20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE text_blocks \n SET title = COALESCE($1, title),\n markdown = COALESCE($2, markdown),\n block_order = COALESCE($3, block_order),\n is_active = COALESCE($4, is_active),\n updated_at = NOW()\n WHERE id = $5\n RETURNING id, page_id, block_order, title, markdown, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "markdown", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Text", - "Int4", - "Bool", - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false - ] - }, - "hash": "20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6" -} diff --git a/.sqlx/query-250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef.json b/.sqlx/query-250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef.json deleted file mode 100644 index bbbe30f..0000000 --- a/.sqlx/query-250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE chart_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Int4", - "Uuid", - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef" -} diff --git a/.sqlx/query-285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0.json b/.sqlx/query-285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0.json deleted file mode 100644 index 987f8ac..0000000 --- a/.sqlx/query-285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, \"order\"\n FROM table_columns WHERE table_block_id = $1 ORDER BY \"order\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "table_block_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "label", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "data_type", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "is_sortable", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "is_filterable", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "width", - "type_info": "Int4" - }, - { - "ordinal": 8, - "name": "order", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - false - ] - }, - "hash": "285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0" -} diff --git a/.sqlx/query-2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab.json b/.sqlx/query-2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab.json deleted file mode 100644 index ea8ecc8..0000000 --- a/.sqlx/query-2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT \n COUNT(*) as total_count,\n COUNT(CASE WHEN is_system = true THEN 1 END) as system_count,\n COUNT(CASE WHEN is_editable = true THEN 1 END) as editable_count\n FROM settings \n WHERE category = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "total_count", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "system_count", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "editable_count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - null, - null, - null - ] - }, - "hash": "2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab" -} diff --git a/.sqlx/query-32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d.json b/.sqlx/query-32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d.json deleted file mode 100644 index 76e396b..0000000 --- a/.sqlx/query-32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM hero_blocks WHERE page_id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d" -} diff --git a/.sqlx/query-34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66.json b/.sqlx/query-34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66.json deleted file mode 100644 index 5eaa4a3..0000000 --- a/.sqlx/query-34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) as count FROM hero_blocks WHERE page_id = $1 AND is_active = true", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - null - ] - }, - "hash": "34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66" -} diff --git a/.sqlx/query-36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa.json b/.sqlx/query-36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa.json deleted file mode 100644 index e7586fd..0000000 --- a/.sqlx/query-36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM settings_blocks WHERE page_id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa" -} diff --git a/.sqlx/query-37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8.json b/.sqlx/query-37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8.json deleted file mode 100644 index d797edb..0000000 --- a/.sqlx/query-37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, \"order\"\n FROM table_columns WHERE table_block_id = $1 ORDER BY \"order\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "table_block_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "label", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "data_type", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "is_sortable", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "is_filterable", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "width", - "type_info": "Int4" - }, - { - "ordinal": 8, - "name": "order", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - false - ] - }, - "hash": "37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8" -} diff --git a/.sqlx/query-37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b.json b/.sqlx/query-37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b.json deleted file mode 100644 index 1bcc331..0000000 --- a/.sqlx/query-37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE text_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Int4", - "Uuid", - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b" -} diff --git a/.sqlx/query-3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82.json b/.sqlx/query-3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82.json deleted file mode 100644 index 7ba81cd..0000000 --- a/.sqlx/query-3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM table_blocks WHERE id = $1 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82" -} diff --git a/.sqlx/query-45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d.json b/.sqlx/query-45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d.json deleted file mode 100644 index 3e84f4a..0000000 --- a/.sqlx/query-45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM table_blocks WHERE page_id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d" -} diff --git a/.sqlx/query-4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b.json b/.sqlx/query-4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b.json deleted file mode 100644 index 24bd2b8..0000000 --- a/.sqlx/query-4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO settings_blocks (page_id, block_order, title, category, editable, display_mode, is_active)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "category", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "editable", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "display_mode", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 8, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Int4", - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Bool" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false, - false, - false - ] - }, - "hash": "4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b" -} diff --git a/.sqlx/query-4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3.json b/.sqlx/query-4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3.json deleted file mode 100644 index 6b16447..0000000 --- a/.sqlx/query-4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE hero_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Int4", - "Uuid", - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3" -} diff --git a/.sqlx/query-4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067.json b/.sqlx/query-4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067.json deleted file mode 100644 index 94edbe6..0000000 --- a/.sqlx/query-4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO chart_blocks (page_id, block_order, title, chart_type, config, is_active)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id, page_id, block_order, title, chart_type, config, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "chart_type", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "config", - "type_info": "Jsonb" - }, - { - "ordinal": 6, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Int4", - "Varchar", - "Varchar", - "Jsonb", - "Bool" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false - ] - }, - "hash": "4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067" -} diff --git a/.sqlx/query-5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe.json b/.sqlx/query-5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe.json deleted file mode 100644 index afcda0d..0000000 --- a/.sqlx/query-5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) as count FROM settings_blocks WHERE page_id = $1 AND is_active = true", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - null - ] - }, - "hash": "5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe" -} diff --git a/.sqlx/query-6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251.json b/.sqlx/query-6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251.json deleted file mode 100644 index e73cc03..0000000 --- a/.sqlx/query-6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO data_points (chart_block_id, x, y, label, color)\n VALUES ($1, $2, $3, $4, $5)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Float8", - "Float8", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - }, - "hash": "6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251" -} diff --git a/.sqlx/query-7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb.json b/.sqlx/query-7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb.json deleted file mode 100644 index ed5e47f..0000000 --- a/.sqlx/query-7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \n created_by, updated_by\n FROM pages WHERE id = $1 AND is_active = true\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "created_by", - "type_info": "Uuid" - }, - { - "ordinal": 8, - "name": "updated_by", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - true, - true - ] - }, - "hash": "7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb" -} diff --git a/.sqlx/query-74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421.json b/.sqlx/query-74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421.json deleted file mode 100644 index 5489283..0000000 --- a/.sqlx/query-74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM pages WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421" -} diff --git a/.sqlx/query-74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b.json b/.sqlx/query-74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b.json deleted file mode 100644 index 495bca7..0000000 --- a/.sqlx/query-74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM text_blocks WHERE id = $1 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b" -} diff --git a/.sqlx/query-76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922.json b/.sqlx/query-76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922.json deleted file mode 100644 index de32bb8..0000000 --- a/.sqlx/query-76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM chart_blocks WHERE page_id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922" -} diff --git a/.sqlx/query-76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc.json b/.sqlx/query-76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc.json deleted file mode 100644 index 707ec0e..0000000 --- a/.sqlx/query-76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE hero_blocks \n SET title = COALESCE($1, title),\n subtitle = COALESCE($2, subtitle),\n background_image = COALESCE($3, background_image),\n background_color = COALESCE($4, background_color),\n text_color = COALESCE($5, text_color),\n cta_text = COALESCE($6, cta_text),\n cta_link = COALESCE($7, cta_link),\n block_order = COALESCE($8, block_order),\n is_active = COALESCE($9, is_active),\n updated_at = NOW()\n WHERE id = $10\n RETURNING id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "subtitle", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "background_image", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "background_color", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "text_color", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "cta_text", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "cta_link", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 11, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 12, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Text", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Int4", - "Bool", - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false - ] - }, - "hash": "76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc" -} diff --git a/.sqlx/query-79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786.json b/.sqlx/query-79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786.json deleted file mode 100644 index 6e2a625..0000000 --- a/.sqlx/query-79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE table_blocks \n SET title = COALESCE($1, title),\n data_source = COALESCE($2, data_source),\n data_config = COALESCE($3, data_config),\n block_order = COALESCE($4, block_order),\n is_active = COALESCE($5, is_active),\n updated_at = NOW()\n WHERE id = $6\n RETURNING id, page_id, block_order, title, data_source, data_config, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "data_source", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "data_config", - "type_info": "Jsonb" - }, - { - "ordinal": 6, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Jsonb", - "Int4", - "Bool", - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - true, - false, - false, - false - ] - }, - "hash": "79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786" -} diff --git a/.sqlx/query-7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03.json b/.sqlx/query-7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03.json deleted file mode 100644 index 60f9127..0000000 --- a/.sqlx/query-7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM chart_blocks WHERE id = $1 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03" -} diff --git a/.sqlx/query-7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea.json b/.sqlx/query-7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea.json deleted file mode 100644 index 4db9dcc..0000000 --- a/.sqlx/query-7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) as count FROM chart_blocks WHERE page_id = $1 AND is_active = true", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - null - ] - }, - "hash": "7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea" -} diff --git a/.sqlx/query-8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad.json b/.sqlx/query-8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad.json deleted file mode 100644 index 4b538f9..0000000 --- a/.sqlx/query-8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE pages \n SET title = COALESCE($1, title),\n slug = COALESCE($2, slug),\n description = COALESCE($3, description),\n is_active = COALESCE($4, is_active),\n updated_at = NOW(),\n updated_by = $5\n WHERE id = $6\n RETURNING id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \n created_by, updated_by\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "created_by", - "type_info": "Uuid" - }, - { - "ordinal": 8, - "name": "updated_by", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Text", - "Bool", - "Uuid", - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - true, - true - ] - }, - "hash": "8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad" -} diff --git a/.sqlx/query-8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f.json b/.sqlx/query-8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f.json deleted file mode 100644 index cc66807..0000000 --- a/.sqlx/query-8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO pages (title, slug, description, is_active, created_by)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \n created_by, updated_by\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "created_by", - "type_info": "Uuid" - }, - { - "ordinal": 8, - "name": "updated_by", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Text", - "Bool", - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - true, - true - ] - }, - "hash": "8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f" -} diff --git a/.sqlx/query-8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e.json b/.sqlx/query-8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e.json deleted file mode 100644 index 307e6ec..0000000 --- a/.sqlx/query-8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \n created_by, updated_by\n FROM pages \n WHERE is_active = true \n AND (title ILIKE $1 OR description ILIKE $1 OR slug ILIKE $1)\n ORDER BY \n CASE \n WHEN title ILIKE $1 THEN 1\n WHEN slug ILIKE $1 THEN 2\n ELSE 3\n END,\n updated_at DESC\n LIMIT $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "created_by", - "type_info": "Uuid" - }, - { - "ordinal": 8, - "name": "updated_by", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Text", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - true, - true - ] - }, - "hash": "8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e" -} diff --git a/.sqlx/query-911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7.json b/.sqlx/query-911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7.json deleted file mode 100644 index eb42654..0000000 --- a/.sqlx/query-911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM data_points WHERE chart_block_id IN (SELECT id FROM chart_blocks WHERE page_id = $1)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7" -} diff --git a/.sqlx/query-94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb.json b/.sqlx/query-94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb.json deleted file mode 100644 index 82543c1..0000000 --- a/.sqlx/query-94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) as count FROM table_blocks WHERE page_id = $1 AND is_active = true", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - null - ] - }, - "hash": "94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb" -} diff --git a/.sqlx/query-94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da.json b/.sqlx/query-94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da.json deleted file mode 100644 index 9ca93a7..0000000 --- a/.sqlx/query-94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE table_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Int4", - "Uuid", - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da" -} diff --git a/.sqlx/query-97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa.json b/.sqlx/query-97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa.json deleted file mode 100644 index 475e25b..0000000 --- a/.sqlx/query-97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO data_points (chart_block_id, x, y, label, color)\n VALUES ($1, $2, $3, $4, $5)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Float8", - "Float8", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - }, - "hash": "97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa" -} diff --git a/.sqlx/query-a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d.json b/.sqlx/query-a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d.json deleted file mode 100644 index 1c25900..0000000 --- a/.sqlx/query-a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, chart_block_id, x, y, label, color\n FROM data_points WHERE chart_block_id = $1 ORDER BY x\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "chart_block_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "x", - "type_info": "Float8" - }, - { - "ordinal": 3, - "name": "y", - "type_info": "Float8" - }, - { - "ordinal": 4, - "name": "label", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "color", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - true - ] - }, - "hash": "a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d" -} diff --git a/.sqlx/query-a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf.json b/.sqlx/query-a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf.json deleted file mode 100644 index de1d825..0000000 --- a/.sqlx/query-a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE settings_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Int4", - "Uuid", - "Uuid" - ] - }, - "nullable": [ - false - ] - }, - "hash": "a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf" -} diff --git a/.sqlx/query-ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f.json b/.sqlx/query-ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f.json deleted file mode 100644 index 1e96cd1..0000000 --- a/.sqlx/query-ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, page_id, block_order, title, chart_type, config, is_active, created_at as \"created_at: DateTime\", updated_at as \"updated_at: DateTime\"\n FROM chart_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "chart_type", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "config", - "type_info": "Jsonb" - }, - { - "ordinal": 6, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false - ] - }, - "hash": "ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f" -} diff --git a/.sqlx/query-bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28.json b/.sqlx/query-bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28.json deleted file mode 100644 index b053332..0000000 --- a/.sqlx/query-bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, page_id, block_order, title, data_source, data_config, is_active, created_at as \"created_at: DateTime\", updated_at as \"updated_at: DateTime\"\n FROM table_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "data_source", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "data_config", - "type_info": "Jsonb" - }, - { - "ordinal": 6, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - true, - false, - false, - false - ] - }, - "hash": "bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28" -} diff --git a/.sqlx/query-c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7.json b/.sqlx/query-c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7.json deleted file mode 100644 index 7ecbaef..0000000 --- a/.sqlx/query-c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \n created_by, updated_by\n FROM pages WHERE slug = $1 AND is_active = true\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "created_by", - "type_info": "Uuid" - }, - { - "ordinal": 8, - "name": "updated_by", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - true, - true - ] - }, - "hash": "c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7" -} diff --git a/.sqlx/query-c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640.json b/.sqlx/query-c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640.json deleted file mode 100644 index 0fde01d..0000000 --- a/.sqlx/query-c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE settings_blocks \n SET title = COALESCE($1, title),\n category = COALESCE($2, category),\n editable = COALESCE($3, editable),\n display_mode = COALESCE($4, display_mode),\n block_order = COALESCE($5, block_order),\n is_active = COALESCE($6, is_active),\n updated_at = NOW()\n WHERE id = $7\n RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "category", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "editable", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "display_mode", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 8, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Bool", - "Varchar", - "Int4", - "Bool", - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false, - false, - false - ] - }, - "hash": "c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640" -} diff --git a/.sqlx/query-cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d.json b/.sqlx/query-cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d.json deleted file mode 100644 index b7d8700..0000000 --- a/.sqlx/query-cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM table_columns WHERE table_block_id IN (SELECT id FROM table_blocks WHERE page_id = $1)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d" -} diff --git a/.sqlx/query-d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de.json b/.sqlx/query-d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de.json deleted file mode 100644 index f8302cd..0000000 --- a/.sqlx/query-d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM text_blocks WHERE page_id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de" -} diff --git a/.sqlx/query-ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994.json b/.sqlx/query-ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994.json deleted file mode 100644 index 32810bf..0000000 --- a/.sqlx/query-ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, page_id, block_order, title, category, editable, display_mode, is_active, created_at as \"created_at: DateTime\", updated_at as \"updated_at: DateTime\"\n FROM settings_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "category", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "editable", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "display_mode", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 8, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false, - false, - false - ] - }, - "hash": "ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994" -} diff --git a/.sqlx/query-e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4.json b/.sqlx/query-e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4.json deleted file mode 100644 index af6361e..0000000 --- a/.sqlx/query-e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM table_columns WHERE table_block_id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4" -} diff --git a/.sqlx/query-ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13.json b/.sqlx/query-ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13.json deleted file mode 100644 index 2047a5f..0000000 --- a/.sqlx/query-ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) as count FROM pages WHERE slug = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - null - ] - }, - "hash": "ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13" -} diff --git a/.sqlx/query-fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad.json b/.sqlx/query-fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad.json deleted file mode 100644 index 6ce954a..0000000 --- a/.sqlx/query-fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) as count FROM pages WHERE slug = $1 AND id != $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Uuid" - ] - }, - "nullable": [ - null - ] - }, - "hash": "fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad" -} diff --git a/.sqlx/query-fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1.json b/.sqlx/query-fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1.json deleted file mode 100644 index 6372dfb..0000000 --- a/.sqlx/query-fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO text_blocks (page_id, block_order, title, markdown, is_active)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, page_id, block_order, title, markdown, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "markdown", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Int4", - "Varchar", - "Text", - "Bool" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false - ] - }, - "hash": "fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1" -} diff --git a/.sqlx/query-fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5.json b/.sqlx/query-fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5.json deleted file mode 100644 index 0cd05e9..0000000 --- a/.sqlx/query-fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE chart_blocks \n SET title = COALESCE($1, title),\n chart_type = COALESCE($2, chart_type),\n config = COALESCE($3, config),\n block_order = COALESCE($4, block_order),\n is_active = COALESCE($5, is_active),\n updated_at = NOW()\n WHERE id = $6\n RETURNING id, page_id, block_order, title, chart_type, config, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\"\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "chart_type", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "config", - "type_info": "Jsonb" - }, - { - "ordinal": 6, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Varchar", - "Varchar", - "Jsonb", - "Int4", - "Bool", - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false - ] - }, - "hash": "fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5" -} diff --git a/.sqlx/query-fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a.json b/.sqlx/query-fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a.json deleted file mode 100644 index 12eee4f..0000000 --- a/.sqlx/query-fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id, page_id, block_order, title, markdown, is_active, created_at as \"created_at: DateTime\", updated_at as \"updated_at: DateTime\"\n FROM text_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "page_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "block_order", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "markdown", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "is_active", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "created_at: DateTime", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "updated_at: DateTime", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false - ] - }, - "hash": "fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a" -} diff --git a/migrations/014_insert_default_settings.sql b/migrations/014_insert_default_settings.sql new file mode 100644 index 0000000..27ad14d --- /dev/null +++ b/migrations/014_insert_default_settings.sql @@ -0,0 +1,132 @@ +-- Rollback: Delete default configuration items +-- Delete all inserted configuration items +DELETE FROM settings WHERE key IN ( + -- Site configuration + 'site.name', + 'site.description', + 'site.keywords', + 'site.url', + 'site.logo', + 'site.copyright', + 'site.icp', + 'site.icp_url', + 'site.color_style', + + -- User configuration + 'user.default_avatar', + 'user.default_role', + 'user.register_invite_code', + 'user.register_email_verification', + 'user.open_login', + 'user.open_reset_password', + + -- Email configuration + 'email.smtp_host', + 'email.smtp_port', + 'email.smtp_user', + 'email.smtp_password', + 'email.smtp_from', + 'email.smtp_from_name', + 'email.smtp_from_email', + 'email.system_template', + + -- Blog configuration + 'blog.default_author', + 'blog.default_category', + 'blog.default_tag', + 'blog.open_comment', + + -- Logging configuration + 'logging.level', + 'logging.max_files', + 'logging.max_file_size', + + -- Cache configuration + 'cache.ttl', + 'cache.max_size', + + -- Feature switches configuration + 'switch.open_register', + 'switch.open_login', + 'switch.open_reset_password', + 'switch.open_comment', + 'switch.open_like', + 'switch.open_share', + 'switch.open_view' +); + +-- Drop unique index (if exists) +DROP INDEX IF EXISTS idx_settings_key_unique; + +-- Insert default configuration items +-- Site configuration +INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES +('site.name', 'Mapp', 'string', 'Site name', 'site', true, true), +('site.description', 'A modern application platform', 'string', 'Site description', 'site', true, true), +('site.keywords', 'mapp,application,platform', 'string', 'Site keywords', 'site', true, true), +('site.url', 'http://localhost:3000', 'string', 'Site URL', 'site', true, true), +('site.logo', '/static/logo.png', 'string', 'Site logo path', 'site', true, true), +('site.copyright', '© 2024 Mapp. All rights reserved.', 'string', 'Copyright information', 'site', true, true), +('site.icp', '', 'string', 'ICP registration number', 'site', true, true), +('site.icp_url', '', 'string', 'ICP registration URL', 'site', true, true), +('site.color_style', 'default', 'string', 'Site color scheme', 'site', true, true); + +-- User configuration +INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES +('user.default_avatar', '/static/default-avatar.png', 'string', 'Default user avatar', 'user', true, true), +('user.default_role', 'User', 'string', 'Default user role', 'user', true, true), +('user.register_invite_code', 'true', 'boolean', 'Require invite code for registration', 'user', true, true), +('user.register_email_verification', 'false', 'boolean', 'Require email verification for registration', 'user', true, true), +('user.open_login', 'true', 'boolean', 'Enable login', 'user', true, true), +('user.open_reset_password', 'true', 'boolean', 'Enable password reset', 'user', true, true); + +-- Email configuration +INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES +('email.smtp_host', '', 'string', 'SMTP server address', 'email', true, true), +('email.smtp_port', '587', 'number', 'SMTP server port', 'email', true, true), +('email.smtp_user', '', 'string', 'SMTP username', 'email', true, true), +('email.smtp_password', '', 'string', 'SMTP password', 'email', true, true), +('email.smtp_from', '', 'string', 'Sender email', 'email', true, true), +('email.smtp_from_name', 'Mapp System', 'string', 'Sender name', 'email', true, true), +('email.smtp_from_email', '', 'string', 'Sender email address', 'email', true, true), +('email.system_template', 'default', 'string', 'System email template', 'email', true, true); + +-- Blog configuration +INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES +('blog.default_author', 'System', 'string', 'Default blog author', 'blog', true, true), +('blog.default_category', 'Uncategorized', 'string', 'Default blog category', 'blog', true, true), +('blog.default_tag', 'Default', 'string', 'Default blog tag', 'blog', true, true), +('blog.open_comment', 'true', 'boolean', 'Enable comments', 'blog', true, true); + +-- Logging configuration +INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES +('logging.level', 'info', 'string', 'Log level', 'logging', true, true), +('logging.max_files', '10', 'number', 'Maximum log files', 'logging', true, true), +('logging.max_file_size', '10485760', 'number', 'Maximum log file size (bytes)', 'logging', true, true); + +-- Cache configuration +INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES +('cache.ttl', '3600', 'number', 'Cache time to live (seconds)', 'cache', true, true), +('cache.max_size', '1000', 'number', 'Maximum cache entries', 'cache', true, true); + +-- Feature switches configuration +INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES +('switch.open_register', 'true', 'boolean', 'Enable registration', 'switch', true, true), +('switch.open_login', 'true', 'boolean', 'Enable login', 'switch', true, true), +('switch.open_reset_password', 'true', 'boolean', 'Enable password reset', 'switch', true, true), +('switch.open_comment', 'true', 'boolean', 'Enable comments', 'switch', true, true), +('switch.open_like', 'true', 'boolean', 'Enable likes', 'switch', true, true), +('switch.open_share', 'true', 'boolean', 'Enable sharing', 'switch', true, true), +('switch.open_view', 'true', 'boolean', 'Enable view statistics', 'switch', true, true); + +-- 创建唯一索引约束(如果不存在) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_indexes + WHERE indexname = 'idx_settings_key_unique' + AND tablename = 'settings' + ) THEN + CREATE UNIQUE INDEX idx_settings_key_unique ON settings(key); + END IF; +END $$; \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index e62bb49..da55334 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,6 @@ use async_graphql::{ use async_graphql_axum::{GraphQLRequest, GraphQLResponse, GraphQLSubscription}; use axum::{ extract::{FromRef, State}, - http::Method, response::{Html, IntoResponse}, routing::get, Router, @@ -24,10 +23,10 @@ use crate::{ config::Config, graphql::{subscription::StatusUpdate, MutationRoot, QueryRoot, SubscriptionRoot}, services::{ - blog_service::BlogService, casbin_service::CasbinService, - invite_code_service::InviteCodeService, mosaic_service::MosaicService, - page_block_service::PageBlockService, settings_service::SettingsService, - system_config_service::SystemConfigService, user_service::UserService, + blog_service::BlogService, casbin_service::CasbinService, config_manager::ConfigsManager, + config_service::ConfigsService, invite_code_service::InviteCodeService, + mosaic_service::MosaicService, system_config_service::SystemConfigService, + user_service::UserService, }, }; @@ -48,31 +47,36 @@ pub async fn create_router( config: Config, status_sender: Option>, ) -> Router { - let user_service = UserService::new(pool.clone(), config.jwt_secret.clone()); - let invite_code_service = InviteCodeService::new(pool.clone()); - let system_config_service = SystemConfigService::new(pool.clone()); - let mosaic_service = MosaicService::new(pool.clone()); - let settings_service = SettingsService::new(pool.clone()); - let page_block_service = PageBlockService::new(pool.clone()); - let blog_service = BlogService::new(pool.clone()); - let casbin_service = CasbinService::new(config.database_url.clone()) .await .expect("Failed to initialize CasbinService"); - let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot) - .data(pool) - .data(user_service) - .data(invite_code_service) - .data(system_config_service) - .data(mosaic_service) - .data(settings_service) - .data(page_block_service) - .data(blog_service) - .data(casbin_service) - .data(config.clone()) - .data(status_sender.clone()) - .finish(); + let user_service = UserService::new(pool.clone(), config.jwt_secret.clone()) + .with_casbin(casbin_service.clone()); + + let invite_code_service = InviteCodeService::new(pool.clone()); + let system_config_service = SystemConfigService::new(pool.clone()); + let mosaic_service = MosaicService::new(pool.clone()); + let configs_service = ConfigsService::new(pool.clone()); + let config_manager = ConfigsManager::new(configs_service).await; + let blog_service = BlogService::new(pool.clone()); + + let schema = Schema::build( + QueryRoot::default(), + MutationRoot::default(), + SubscriptionRoot, + ) + .data(pool) + .data(user_service) + .data(invite_code_service) + .data(system_config_service) + .data(mosaic_service) + .data(config_manager) + .data(blog_service) + .data(casbin_service) + .data(config.clone()) + .data(status_sender.clone()) + .finish(); let keys = vec![DecodingKey::from_secret(config.jwt_secret.as_bytes())]; let validation = Validation::default(); @@ -94,19 +98,19 @@ pub async fn create_router( Router::new() .route("/", get(graphql_playground)) .route("/graphql", get(graphql_playground).post(graphql_handler)) - .route_layer( - RateLimitLayer::::builder() - .with_route( - (Method::GET, "/graphql"), - Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()), - ) - .with_route( - (Method::POST, "/graphql"), - Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()), - ) - .with_gc_interval(1000) - .default_handle_error(), - ) + // .route_layer( + // RateLimitLayer::::builder() + // .with_route( + // (Method::GET, "/graphql"), + // Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()), + // ) + // .with_route( + // (Method::POST, "/graphql"), + // Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()), + // ) + // .with_gc_interval(1000) + // .default_handle_error(), + // ) .route_service("/ws", GraphQLSubscription::new(schema)) .layer(CorsLayer::permissive()) .merge(router) diff --git a/src/graphql/guards.rs b/src/graphql/guards.rs index 4c842a4..47b3636 100644 --- a/src/graphql/guards.rs +++ b/src/graphql/guards.rs @@ -3,6 +3,15 @@ use crate::{auth::get_auth_user, services::casbin_service::CasbinService}; use async_graphql::{Context, Error, Guard, Result}; use tracing::warn; +pub struct RequireLogin; + +impl Guard for RequireLogin { + async fn check(&self, ctx: &Context<'_>) -> Result<()> { + get_auth_user(ctx).await?; + Ok(()) + } +} + pub struct RequireRole(pub Role); impl Guard for RequireRole { diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index c28df9a..98a8ee4 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -1,9 +1,9 @@ pub mod guards; -pub mod mutation; -pub mod query; +pub mod mutations; +pub mod queries; pub mod subscription; pub mod types; -pub use mutation::MutationRoot; -pub use query::QueryRoot; -pub use subscription::SubscriptionRoot; \ No newline at end of file +pub use mutations::MutationRoot; +pub use queries::QueryRoot; +pub use subscription::SubscriptionRoot; diff --git a/src/graphql/mutation.rs b/src/graphql/mutation.rs deleted file mode 100644 index bcf60e8..0000000 --- a/src/graphql/mutation.rs +++ /dev/null @@ -1,1313 +0,0 @@ -use crate::auth::get_auth_user; -use crate::graphql::guards::{RequireReadPermission, RequireRole, RequireWritePermission}; -use crate::graphql::types::ConfigUpdateResultType; -use crate::graphql::types::UpdateDocsSupportInput; -use crate::graphql::types::UpdateModalAnnouncementInput; -use crate::graphql::types::UpdateNoticeConfigInput; -use crate::graphql::types::UpdateOpsConfigInput; -use crate::graphql::types::UpdateSiteConfigInput; -use crate::graphql::types::*; -use crate::models::blog::{ - CreateBlogCategoryInput, CreateBlogInput, CreateBlogTagInput, UpdateBlogCategoryInput, - UpdateBlogInput, UpdateBlogTagInput, -}; -use crate::models::page_block::*; -use crate::models::settings::{CreateSetting, UpdateSetting}; -use crate::models::user::Role; -use crate::models::user::User; -use crate::models::Blog; -use crate::models::BlogCategory; -use crate::models::BlogTag; -use crate::services::blog_service::BlogService; -use crate::services::casbin_service::CasbinService; -use crate::services::invite_code_service::InviteCodeService; -use crate::services::page_block_service::PageBlockService; -use crate::services::settings_service::SettingsService; -use crate::services::user_service::UserService; -use async_graphql::Error as GraphQLError; -use async_graphql::{Context, Object, Result}; -use uuid::Uuid; - -pub struct MutationRoot; - -#[Object] -impl MutationRoot { - async fn register(&self, ctx: &Context<'_>, input: RegisterInput) -> Result { - let user_service = ctx.data::()?; - user_service.register(input).await - } - - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn create_user(&self, ctx: &Context<'_>, input: CreateUserInput) -> Result { - let user_service = ctx.data::()?; - user_service.create_user(input).await - } - - async fn login(&self, ctx: &Context<'_>, input: LoginInput) -> Result { - let user_service = ctx.data::()?; - user_service.login(input).await - } - - async fn create_invite_code( - &self, - ctx: &Context<'_>, - input: CreateInviteCodeInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let invite_code_service = ctx.data::()?; - - let code = invite_code_service - .create_invite_code(auth_user.id, input) - .await?; - - // Get the invite code details to return expires_at - let invite_codes = invite_code_service - .get_invite_codes_by_creator(auth_user.id) - .await?; - let invite_code = invite_codes - .into_iter() - .find(|ic| ic.code == code) - .ok_or_else(|| async_graphql::Error::new("Failed to retrieve created invite code"))?; - - Ok(InviteCodeResponse { - code, - expires_at: invite_code.expires_at, - }) - } - - async fn initialize_admin( - &self, - ctx: &Context<'_>, - input: InitializeAdminInput, - ) -> Result { - let user_service = ctx.data::()?; - - match user_service - .initialize_admin(input.username, input.email, input.password) - .await - { - Ok(user) => Ok(InitializeAdminResponse { - success: true, - message: "Admin user initialized successfully".to_string(), - user: Some(user), - }), - Err(e) => Ok(InitializeAdminResponse { - success: false, - message: e.message, - user: None, - }), - } - } - - // Settings mutations - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn create_setting( - &self, - ctx: &Context<'_>, - input: CreateSettingInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let settings_service = ctx.data::()?; - - let create_setting = CreateSetting { - key: input.key, - value: input.value, - value_type: input.value_type, - description: input.description, - category: input.category, - is_encrypted: input.is_encrypted, - is_system: input.is_system, - is_editable: input.is_editable, - }; - - let setting = settings_service - .create_setting(create_setting, auth_user.id) - .await?; - - Ok(SettingType { - id: setting.id, - key: setting.key, - value: setting.value, - value_type: setting.value_type, - description: setting.description, - category: setting.category, - is_encrypted: setting.is_encrypted, - is_system: setting.is_system, - is_editable: setting.is_editable, - created_at: setting.created_at, - updated_at: setting.updated_at, - created_by: setting.created_by, - updated_by: setting.updated_by, - }) - } - - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn update_setting( - &self, - ctx: &Context<'_>, - id: Uuid, - input: UpdateSettingInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let settings_service = ctx.data::()?; - - let update_setting = UpdateSetting { - value: input.value, - description: input.description, - category: input.category, - is_editable: input.is_editable, - }; - - let setting = settings_service - .update_setting(id, update_setting, auth_user.id) - .await?; - - Ok(SettingType { - id: setting.id, - key: setting.key, - value: setting.value, - value_type: setting.value_type, - description: setting.description, - category: setting.category, - is_encrypted: setting.is_encrypted, - is_system: setting.is_system, - is_editable: setting.is_editable, - created_at: setting.created_at, - updated_at: setting.updated_at, - created_by: setting.created_by, - updated_by: setting.updated_by, - }) - } - - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn delete_setting(&self, ctx: &Context<'_>, id: Uuid) -> Result { - let settings_service = ctx.data::()?; - let deleted = settings_service.delete_setting(id).await?; - Ok(deleted) - } - - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn batch_update_settings( - &self, - ctx: &Context<'_>, - input: BatchUpdateSettingsInput, - ) -> Result> { - let auth_user = get_auth_user(ctx).await?; - let settings_service = ctx.data::()?; - - let updates: Vec<(String, serde_json::Value)> = input - .updates - .into_iter() - .map(|item| { - ( - item.key, - item.value - .map(|v| serde_json::Value::String(v)) - .unwrap_or(serde_json::Value::Null), - ) - }) - .collect(); - - let settings = settings_service - .batch_update_settings(updates, auth_user.id) - .await?; - - Ok(settings - .into_iter() - .map(|s| SettingType { - id: s.id, - key: s.key, - value: s.value, - value_type: s.value_type, - description: s.description, - category: s.category, - is_encrypted: s.is_encrypted, - is_system: s.is_system, - is_editable: s.is_editable, - created_at: s.created_at, - updated_at: s.updated_at, - created_by: s.created_by, - updated_by: s.updated_by, - }) - .collect()) - } - - // Page Block mutations - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn create_page(&self, ctx: &Context<'_>, input: CreatePageInputType) -> Result { - let auth_user = get_auth_user(ctx).await?; - let page_block_service = ctx.data::()?; - - let create_input = CreatePageInput { - title: input.title, - slug: input.slug, - description: input.description, - is_active: input.is_active, - }; - - let page = page_block_service - .create_page(create_input, auth_user.id) - .await?; - - Ok(PageType { - id: page.id, - title: page.title, - slug: page.slug, - description: page.description, - is_active: page.is_active, - created_at: page.created_at, - updated_at: page.updated_at, - created_by: page.created_by, - updated_by: page.updated_by, - }) - } - - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn create_text_block( - &self, - ctx: &Context<'_>, - input: CreateTextBlockInputType, - ) -> Result { - let page_block_service = ctx.data::()?; - - let create_input = CreateTextBlockInput { - page_id: input.page_id, - block_order: input.block_order, - title: input.title, - markdown: input.markdown, - is_active: input.is_active, - }; - - let block = page_block_service.create_text_block(create_input).await?; - - Ok(TextBlockType { - id: block.id, - page_id: block.page_id, - block_order: block.block_order, - title: block.title, - markdown: block.markdown, - is_active: block.is_active, - created_at: block.created_at, - updated_at: block.updated_at, - }) - } - - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn create_chart_block( - &self, - ctx: &Context<'_>, - input: CreateChartBlockInputType, - ) -> Result { - let page_block_service = ctx.data::()?; - - let create_input = CreateChartBlockInput { - page_id: input.page_id, - block_order: input.block_order, - title: input.title, - chart_type: input.chart_type, - series: input - .series - .into_iter() - .map(|dp| CreateDataPointInput { - x: dp.x, - y: dp.y, - label: dp.label, - color: dp.color, - }) - .collect(), - config: input.config, - is_active: input.is_active, - }; - - let block = page_block_service.create_chart_block(create_input).await?; - - Ok(ChartBlockType { - id: block.id, - page_id: block.page_id, - block_order: block.block_order, - title: block.title, - chart_type: block.chart_type, - series: block - .series - .into_iter() - .map(|dp| DataPointType { - id: dp.id, - chart_block_id: dp.chart_block_id, - x: dp.x, - y: dp.y, - label: dp.label, - color: dp.color, - }) - .collect(), - config: block.config, - is_active: block.is_active, - created_at: block.created_at, - updated_at: block.updated_at, - }) - } - - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn create_settings_block( - &self, - ctx: &Context<'_>, - input: CreateSettingsBlockInputType, - ) -> Result { - let page_block_service = ctx.data::()?; - - let create_input = CreateSettingsBlockInput { - page_id: input.page_id, - block_order: input.block_order, - title: input.title, - category: input.category, - editable: input.editable, - display_mode: input.display_mode, - is_active: input.is_active, - }; - - let block = page_block_service - .create_settings_block(create_input) - .await?; - - Ok(SettingsBlockType { - id: block.id, - page_id: block.page_id, - block_order: block.block_order, - title: block.title, - category: block.category, - editable: block.editable, - display_mode: block.display_mode, - is_active: block.is_active, - created_at: block.created_at, - updated_at: block.updated_at, - }) - } - - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn delete_page(&self, ctx: &Context<'_>, page_id: Uuid) -> Result { - let page_block_service = ctx.data::()?; - - page_block_service.delete_page(page_id).await?; - Ok(true) - } - - // Enhanced Settings mutations for settings center - #[graphql(guard = "RequireRole(Role::Admin)")] - async fn update_settings( - &self, - ctx: &Context<'_>, - input: BatchUpdateSettingsInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let settings_service = ctx.data::()?; - - // 开始事务 - let mut tx = settings_service.get_pool().begin().await?; - - for update in input.updates { - // 更新设置 - let update_input = UpdateSetting { - value: update.value, - description: update.description, - category: None, - is_editable: None, - }; - - // 这里需要先获取设置的ID,然后更新 - // 暂时跳过,因为update_setting_by_key方法不存在 - // TODO: 实现通过key更新设置的功能 - tracing::warn!("跳过设置更新: {}", update.key); - } - - // 记录变更原因到历史表 - if let Some(reason) = input.reason { - // 这里可以添加变更历史记录逻辑 - tracing::info!("批量更新设置,原因: {}", reason); - } - - // 提交事务 - tx.commit().await?; - - Ok(true) - } - - // 权限管理相关的 Mutation - #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] - async fn assign_role_to_user( - &self, - ctx: &Context<'_>, - user_id: Uuid, - role_name: String, - ) -> Result { - let casbin_service = ctx.data::()?; - - casbin_service - .assign_role(&user_id.to_string(), &role_name) - .await?; - - Ok(true) - } - - #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] - async fn remove_role_from_user( - &self, - ctx: &Context<'_>, - user_id: Uuid, - role_name: String, - ) -> Result { - let casbin_service = ctx.data::()?; - - casbin_service - .remove_role(&user_id.to_string(), &role_name) - .await?; - - Ok(true) - } - - #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] - async fn add_policy( - &self, - ctx: &Context<'_>, - role_name: String, - resource: String, - action: String, - ) -> Result { - let casbin_service = ctx.data::()?; - - casbin_service - .add_policy(&role_name, &resource, &action) - .await?; - - Ok(true) - } - - #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] - async fn remove_policy( - &self, - ctx: &Context<'_>, - role_name: String, - resource: String, - action: String, - ) -> Result { - let casbin_service = ctx.data::()?; - - casbin_service - .remove_policy(&role_name, &resource, &action) - .await?; - - Ok(true) - } - - #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] - async fn reload_policies(&self, ctx: &Context<'_>) -> Result { - let casbin_service = ctx.data::()?; - - casbin_service.reload_policy().await?; - - Ok(true) - } - - // 站点与运营配置变更方法 - - /// 更新站点配置 - #[graphql(guard = "RequireWritePermission::new(\"settings\")")] - async fn update_site_config( - &self, - ctx: &Context<'_>, - input: UpdateSiteConfigInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let settings_service = ctx.data::()?; - let mut updated_settings = Vec::new(); - - // 更新站点名称 - if let Some(name) = input.name { - if let Some(setting) = settings_service.get_setting_by_key("site.name").await? { - let update = UpdateSetting { - value: Some(name), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新默认语言 - if let Some(locale) = input.locale_default { - if let Some(setting) = settings_service - .get_setting_by_key("site.locale_default") - .await? - { - let update = UpdateSetting { - value: Some(locale), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新支持的语言列表 - if let Some(locales) = input.locales_supported { - if let Some(setting) = settings_service - .get_setting_by_key("site.locales_supported") - .await? - { - let value = serde_json::to_string(&locales)?; - let update = UpdateSetting { - value: Some(value), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新Logo URL - if let Some(logo_url) = input.logo_url { - if let Some(setting) = settings_service - .get_setting_by_key("site.brand.logo_url") - .await? - { - let update = UpdateSetting { - value: Some(logo_url), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新主题色 - if let Some(primary_color) = input.primary_color { - if let Some(setting) = settings_service - .get_setting_by_key("site.brand.primary_color") - .await? - { - let update = UpdateSetting { - value: Some(primary_color), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新暗黑模式默认设置 - if let Some(dark_mode_default) = input.dark_mode_default { - if let Some(setting) = settings_service - .get_setting_by_key("site.brand.dark_mode_default") - .await? - { - let update = UpdateSetting { - value: Some(dark_mode_default.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新页脚链接 - if let Some(footer_links) = input.footer_links { - if let Some(setting) = settings_service - .get_setting_by_key("site.footer_links") - .await? - { - let value = serde_json::to_string(&footer_links)?; - let update = UpdateSetting { - value: Some(value), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - Ok(ConfigUpdateResultType { - success: true, - message: "站点配置更新成功".to_string(), - updated_settings: updated_settings - .into_iter() - .map(|s| SettingType { - id: s.id, - key: s.key, - value: s.value, - value_type: s.value_type, - description: s.description, - category: s.category, - is_encrypted: s.is_encrypted, - is_system: s.is_system, - is_editable: s.is_editable, - created_at: s.created_at, - updated_at: s.updated_at, - created_by: s.created_by, - updated_by: s.updated_by, - }) - .collect(), - }) - } - - /// 更新公告配置 - #[graphql(guard = "RequireWritePermission::new(\"settings\")")] - async fn update_notice_config( - &self, - ctx: &Context<'_>, - input: UpdateNoticeConfigInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let settings_service = ctx.data::()?; - let mut updated_settings = Vec::new(); - - // 更新横幅公告开关 - if let Some(enabled) = input.banner_enabled { - if let Some(setting) = settings_service - .get_setting_by_key("notice.banner.enabled") - .await? - { - let update = UpdateSetting { - value: Some(enabled.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新横幅公告文本 - if let Some(text) = input.banner_text { - if let Some(setting) = settings_service - .get_setting_by_key("notice.banner.text") - .await? - { - let value = serde_json::to_string(&text)?; - let update = UpdateSetting { - value: Some(value), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新维护窗口配置 - if let (Some(enabled), start_time, end_time, message) = ( - input.maintenance_enabled, - input.maintenance_start_time, - input.maintenance_end_time, - input.maintenance_message, - ) { - if let Some(setting) = settings_service - .get_setting_by_key("maintenance.window") - .await? - { - let mut config = if let Ok(existing_config) = setting.get_json() { - existing_config - } else { - serde_json::json!({ - "enabled": false, - "start_time": null, - "end_time": null, - "message": {"zh-CN": "系统维护中,请稍后再试", "en": "System maintenance in progress"} - }) - }; - - // 更新配置 - if let Some(obj) = config.as_object_mut() { - obj.insert("enabled".to_string(), serde_json::Value::Bool(enabled)); - - if let Some(start) = start_time { - obj.insert( - "start_time".to_string(), - serde_json::Value::String(start.to_rfc3339()), - ); - } - - if let Some(end) = end_time { - obj.insert( - "end_time".to_string(), - serde_json::Value::String(end.to_rfc3339()), - ); - } - - if let Some(msg) = message { - obj.insert("message".to_string(), serde_json::to_value(msg)?); - } - } - - let value = serde_json::to_string(&config)?; - let update = UpdateSetting { - value: Some(value), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - Ok(ConfigUpdateResultType { - success: true, - message: "公告配置更新成功".to_string(), - updated_settings: updated_settings - .into_iter() - .map(|s| SettingType { - id: s.id, - key: s.key, - value: s.value, - value_type: s.value_type, - description: s.description, - category: s.category, - is_encrypted: s.is_encrypted, - is_system: s.is_system, - is_editable: s.is_editable, - created_at: s.created_at, - updated_at: s.updated_at, - created_by: s.created_by, - updated_by: s.updated_by, - }) - .collect(), - }) - } - - /// 更新弹窗公告 - #[graphql(guard = "RequireWritePermission::new(\"settings\")")] - async fn update_modal_announcement( - &self, - ctx: &Context<'_>, - input: UpdateModalAnnouncementInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let settings_service = ctx.data::()?; - let mut updated_settings = Vec::new(); - - if let Some(setting) = settings_service - .get_setting_by_key("modal.announcements") - .await? - { - let mut announcements = if let Ok(existing) = setting.get_json() { - existing - .as_array() - .map(|arr| arr.to_vec()) - .unwrap_or_default() - } else { - Vec::new() - }; - - // 查找并更新指定的公告 - let mut found = false; - for announcement in &mut announcements { - if let Some(id) = announcement.get("id").and_then(|v| v.as_str()) { - if id == input.id { - found = true; - - // 更新标题 - if let Some(title) = &input.title { - announcement["title"] = serde_json::to_value(title)?; - } - - // 更新内容 - if let Some(content) = &input.content { - announcement["content"] = serde_json::to_value(content)?; - } - - // 更新时间 - if let Some(start_time) = input.start_time { - announcement["start_time"] = - serde_json::Value::String(start_time.to_rfc3339()); - } - - if let Some(end_time) = input.end_time { - announcement["end_time"] = - serde_json::Value::String(end_time.to_rfc3339()); - } - - // 更新受众 - if let Some(audience) = &input.audience { - announcement["audience"] = serde_json::to_value(audience)?; - } - - // 更新优先级 - if let Some(priority) = &input.priority { - announcement["priority"] = serde_json::Value::String(priority.clone()); - } - - break; - } - } - } - - if !found { - return Err(async_graphql::Error::new("未找到指定的弹窗公告")); - } - - let value = serde_json::to_string(&announcements)?; - let update = UpdateSetting { - value: Some(value), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - - Ok(ConfigUpdateResultType { - success: true, - message: "弹窗公告更新成功".to_string(), - updated_settings: updated_settings.into_iter().map(|s| s.into()).collect(), - }) - } - - /// 更新文档支持配置 - #[graphql(guard = "RequireWritePermission::new(\"settings\")")] - async fn update_docs_support_config( - &self, - ctx: &Context<'_>, - input: UpdateDocsSupportInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let settings_service = ctx.data::()?; - let mut updated_settings = Vec::new(); - - // 更新文档链接 - if let Some(links) = input.links { - if let Some(setting) = settings_service.get_setting_by_key("docs.links").await? { - let value = serde_json::to_string(&links)?; - let update = UpdateSetting { - value: Some(value), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新支持渠道 - if let (Some(email), Some(ticket_system), Some(working_hours)) = - (input.email, input.ticket_system, input.working_hours) - { - if let Some(setting) = settings_service - .get_setting_by_key("support.channels") - .await? - { - let mut channels = if let Ok(existing) = setting.get_json() { - existing - } else { - serde_json::json!({ - "email": "support@mapp.com", - "ticket_system": "/support/tickets", - "chat_groups": [], - "working_hours": {"zh-CN": "周一至周五 9:00-18:00", "en": "Mon-Fri 9:00-18:00"} - }) - }; - - if let Some(obj) = channels.as_object_mut() { - obj.insert("email".to_string(), serde_json::Value::String(email)); - obj.insert( - "ticket_system".to_string(), - serde_json::Value::String(ticket_system), - ); - obj.insert( - "working_hours".to_string(), - serde_json::to_value(working_hours)?, - ); - } - - let value = serde_json::to_string(&channels)?; - let update = UpdateSetting { - value: Some(value), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - Ok(ConfigUpdateResultType { - success: true, - message: "文档支持配置更新成功".to_string(), - updated_settings: updated_settings.into_iter().map(|s| s.into()).collect(), - }) - } - - /// 更新运营配置 - #[graphql(guard = "RequireWritePermission::new(\"settings\")")] - async fn update_ops_config( - &self, - ctx: &Context<'_>, - input: UpdateOpsConfigInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let settings_service = ctx.data::()?; - let mut updated_settings = Vec::new(); - - // 更新功能开关 - if let Some(enabled) = input.registration_enabled { - if let Some(setting) = settings_service - .get_setting_by_key("ops.features.registration_enabled") - .await? - { - let update = UpdateSetting { - value: Some(enabled.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - if let Some(required) = input.invite_code_required { - if let Some(setting) = settings_service - .get_setting_by_key("ops.features.invite_code_required") - .await? - { - let update = UpdateSetting { - value: Some(required.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - if let Some(verification) = input.email_verification { - if let Some(setting) = settings_service - .get_setting_by_key("ops.features.email_verification") - .await? - { - let update = UpdateSetting { - value: Some(verification.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新限制配置 - if let Some(max_users) = input.max_users { - if let Some(setting) = settings_service - .get_setting_by_key("ops.limits.max_users") - .await? - { - let update = UpdateSetting { - value: Some(max_users.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - if let Some(max_codes) = input.max_invite_codes_per_user { - if let Some(setting) = settings_service - .get_setting_by_key("ops.limits.max_invite_codes_per_user") - .await? - { - let update = UpdateSetting { - value: Some(max_codes.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - if let Some(timeout) = input.session_timeout_hours { - if let Some(setting) = settings_service - .get_setting_by_key("ops.limits.session_timeout_hours") - .await? - { - let update = UpdateSetting { - value: Some(timeout.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - // 更新通知配置 - if let Some(welcome) = input.welcome_email { - if let Some(setting) = settings_service - .get_setting_by_key("ops.notifications.welcome_email") - .await? - { - let update = UpdateSetting { - value: Some(welcome.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - if let Some(announcements) = input.system_announcements { - if let Some(setting) = settings_service - .get_setting_by_key("ops.notifications.system_announcements") - .await? - { - let update = UpdateSetting { - value: Some(announcements.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - if let Some(alerts) = input.maintenance_alerts { - if let Some(setting) = settings_service - .get_setting_by_key("ops.notifications.maintenance_alerts") - .await? - { - let update = UpdateSetting { - value: Some(alerts.to_string()), - description: None, - category: None, - is_editable: None, - }; - let updated = settings_service - .update_setting(setting.id, update, auth_user.id) - .await?; - updated_settings.push(updated); - } - } - - Ok(ConfigUpdateResultType { - success: true, - message: "运营配置更新成功".to_string(), - updated_settings: updated_settings - .into_iter() - .map(|s| SettingType { - id: s.id, - key: s.key, - value: s.value, - value_type: s.value_type, - description: s.description, - category: s.category, - is_encrypted: s.is_encrypted, - is_system: s.is_system, - is_editable: s.is_editable, - created_at: s.created_at, - updated_at: s.updated_at, - created_by: s.created_by, - updated_by: s.updated_by, - }) - .collect(), - }) - } - - // ==================== Blog 相关变更 ==================== - - /// 创建博客文章 - #[graphql(guard = "RequireWritePermission::new(\"blogs\")")] - async fn create_blog(&self, ctx: &Context<'_>, input: CreateBlogInput) -> Result { - let auth_user = get_auth_user(ctx).await?; - let blog_service = ctx.data::()?; - blog_service - .create_blog(input, auth_user.id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 更新博客文章 - #[graphql(guard = "RequireWritePermission::new(\"blogs\")")] - async fn update_blog( - &self, - ctx: &Context<'_>, - id: Uuid, - input: UpdateBlogInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let blog_service = ctx.data::()?; - blog_service - .update_blog(id, input, auth_user.id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 删除博客文章 - #[graphql(guard = "RequireWritePermission::new(\"blogs\")")] - async fn delete_blog(&self, ctx: &Context<'_>, id: Uuid) -> Result { - let blog_service = ctx.data::()?; - blog_service - .delete_blog(id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 增加博客浏览次数 - #[graphql(guard = "RequireReadPermission::new(\"blogs\")")] - async fn increment_blog_view_count(&self, ctx: &Context<'_>, id: Uuid) -> Result { - let blog_service = ctx.data::()?; - blog_service - .increment_blog_view_count(id) - .await - .map_err(|e| GraphQLError::new(e.to_string()))?; - Ok(true) - } - - /// 创建博客分类 - #[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")] - async fn create_blog_category( - &self, - ctx: &Context<'_>, - input: CreateBlogCategoryInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let blog_service = ctx.data::()?; - blog_service - .create_category(input, auth_user.id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 更新博客分类 - #[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")] - async fn update_blog_category( - &self, - ctx: &Context<'_>, - id: Uuid, - input: UpdateBlogCategoryInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let blog_service = ctx.data::()?; - blog_service - .update_category(id, input, auth_user.id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 删除博客分类 - #[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")] - async fn delete_blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result { - let blog_service = ctx.data::()?; - blog_service - .delete_category(id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 创建博客标签 - #[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")] - async fn create_blog_tag( - &self, - ctx: &Context<'_>, - input: CreateBlogTagInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let blog_service = ctx.data::()?; - blog_service - .create_tag(input, auth_user.id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 更新博客标签 - #[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")] - async fn update_blog_tag( - &self, - ctx: &Context<'_>, - id: Uuid, - input: UpdateBlogTagInput, - ) -> Result { - let auth_user = get_auth_user(ctx).await?; - let blog_service = ctx.data::()?; - blog_service - .update_tag(id, input, auth_user.id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 删除博客标签 - #[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")] - async fn delete_blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result { - let blog_service = ctx.data::()?; - blog_service - .delete_tag(id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } -} diff --git a/src/graphql/mutations.rs b/src/graphql/mutations.rs new file mode 100644 index 0000000..b48e866 --- /dev/null +++ b/src/graphql/mutations.rs @@ -0,0 +1,13 @@ +mod blog; +mod config; +mod permissions; +mod users; +use async_graphql::MergedObject; + +#[derive(MergedObject, Default)] +pub struct MutationRoot( + blog::BlogMutation, + config::ConfigMutation, + permissions::PermissionMutation, + users::UserMutation, +); diff --git a/src/graphql/mutations/blog.rs b/src/graphql/mutations/blog.rs new file mode 100644 index 0000000..ec5139a --- /dev/null +++ b/src/graphql/mutations/blog.rs @@ -0,0 +1,148 @@ +use crate::auth::get_auth_user; +use crate::graphql::guards::*; +use crate::graphql::types::*; +use crate::services::blog_service::BlogService; +use async_graphql::{Context, Error as GraphQLError, Object, Result}; +use uuid::Uuid; + +#[derive(Default)] +pub struct BlogMutation; + +#[Object] +impl BlogMutation { + /// 创建博客文章 + #[graphql(guard = "RequireWritePermission::new(\"blogs\")")] + async fn create_blog(&self, ctx: &Context<'_>, input: CreateBlogInput) -> Result { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .create_blog(input, auth_user.id) + .await + .map(|blog| blog.into()) + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 更新博客文章 + #[graphql(guard = "RequireWritePermission::new(\"blogs\")")] + async fn update_blog( + &self, + ctx: &Context<'_>, + id: Uuid, + input: UpdateBlogInput, + ) -> Result { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .update_blog(id, input, auth_user.id) + .await + .map(|blog| blog.into()) + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 删除博客文章 + #[graphql(guard = "RequireWritePermission::new(\"blogs\")")] + async fn delete_blog(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + blog_service + .delete_blog(id) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 增加博客浏览次数 + #[graphql(guard = "RequireReadPermission::new(\"blogs\")")] + async fn increment_blog_view_count(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + blog_service + .increment_blog_view_count(id) + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + Ok(true) + } + + /// 创建博客分类 + #[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")] + async fn create_blog_category( + &self, + ctx: &Context<'_>, + input: CreateBlogCategoryInput, + ) -> Result { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .create_category(input, auth_user.id) + .await + .map(|category| category.into()) + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 更新博客分类 + #[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")] + async fn update_blog_category( + &self, + ctx: &Context<'_>, + id: Uuid, + input: UpdateBlogCategoryInput, + ) -> Result { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .update_category(id, input, auth_user.id) + .await + .map(|category| category.into()) + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 删除博客分类 + #[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")] + async fn delete_blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + blog_service + .delete_category(id) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 创建博客标签 + #[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")] + async fn create_blog_tag( + &self, + ctx: &Context<'_>, + input: CreateBlogTagInput, + ) -> Result { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .create_tag(input, auth_user.id) + .await + .map(|tag| tag.into()) + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 更新博客标签 + #[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")] + async fn update_blog_tag( + &self, + ctx: &Context<'_>, + id: Uuid, + input: UpdateBlogTagInput, + ) -> Result { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .update_tag(id, input, auth_user.id) + .await + .map(|tag| tag.into()) + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 删除博客标签 + #[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")] + async fn delete_blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + blog_service + .delete_tag(id) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } +} diff --git a/src/graphql/mutations/config.rs b/src/graphql/mutations/config.rs new file mode 100644 index 0000000..7e9221c --- /dev/null +++ b/src/graphql/mutations/config.rs @@ -0,0 +1,33 @@ +use crate::graphql::guards::*; +use crate::graphql::types::*; +use crate::services::config_manager::ConfigsManager; +use async_graphql::{Context, Object, Result}; +use std::collections::HashMap; + +#[derive(Default)] +pub struct ConfigMutation; + +#[Object] +impl ConfigMutation { + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + async fn update_config_batch( + &self, + ctx: &Context<'_>, + input: Vec, + ) -> Result { + let config_manager = ctx.data::()?; + + let configs = input + .into_iter() + .map(|input| { + ( + input.key, + serde_json::to_value(input.value.unwrap()).unwrap(), + ) + }) + .collect::>(); + + config_manager.set_values(configs).await?; + Ok("successed".to_string()) + } +} diff --git a/src/graphql/mutations/permissions.rs b/src/graphql/mutations/permissions.rs new file mode 100644 index 0000000..4e5f072 --- /dev/null +++ b/src/graphql/mutations/permissions.rs @@ -0,0 +1,86 @@ +use crate::graphql::guards::*; +use crate::services::casbin_service::CasbinService; +use async_graphql::{Context, Object, Result}; +use uuid::Uuid; + +#[derive(Default)] +pub struct PermissionMutation; + +#[Object] +impl PermissionMutation { + // 权限管理相关的 Mutation + #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] + async fn assign_role_to_user( + &self, + ctx: &Context<'_>, + user_id: Uuid, + role_name: String, + ) -> Result { + let casbin_service = ctx.data::()?; + + casbin_service + .assign_role(&user_id.to_string(), &role_name) + .await?; + + Ok(true) + } + + #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] + async fn remove_role_from_user( + &self, + ctx: &Context<'_>, + user_id: Uuid, + role_name: String, + ) -> Result { + let casbin_service = ctx.data::()?; + + casbin_service + .remove_role(&user_id.to_string(), &role_name) + .await?; + + Ok(true) + } + + #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] + async fn add_policy( + &self, + ctx: &Context<'_>, + role_name: String, + resource: String, + action: String, + ) -> Result { + let casbin_service = ctx.data::()?; + + casbin_service + .add_policy(&role_name, &resource, &action) + .await?; + + Ok(true) + } + + #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] + async fn remove_policy( + &self, + ctx: &Context<'_>, + role_name: String, + resource: String, + action: String, + ) -> Result { + let casbin_service = ctx.data::()?; + + casbin_service + .remove_policy(&role_name, &resource, &action) + .await?; + + Ok(true) + } + + #[graphql(guard = "RequireWritePermission::new(\"permissions\")")] + async fn reload_policies(&self, ctx: &Context<'_>) -> Result { + let casbin_service = ctx.data::()?; + + casbin_service.reload_policy().await?; + + Ok(true) + } +} diff --git a/src/graphql/mutations/users.rs b/src/graphql/mutations/users.rs new file mode 100644 index 0000000..526eb73 --- /dev/null +++ b/src/graphql/mutations/users.rs @@ -0,0 +1,56 @@ +use crate::graphql::guards::*; +use crate::graphql::types::users::*; +use crate::services::user_service::UserService; +use async_graphql::{Context, Object, Result}; + +#[derive(Default)] +pub struct UserMutation; + +#[Object] +impl UserMutation { + async fn register(&self, ctx: &Context<'_>, input: RegisterInput) -> Result { + let user_service = ctx.data::()?; + user_service.register(input).await.map(|user| user.into()) + } + + #[graphql(guard = "RequireWritePermission::new(\"users\")")] + async fn create_user(&self, ctx: &Context<'_>, input: CreateUserInput) -> Result { + let user_service = ctx.data::()?; + user_service + .create_user(input) + .await + .map(|user| user.into()) + } + + async fn login(&self, ctx: &Context<'_>, input: LoginInput) -> Result { + let user_service = ctx.data::()?; + user_service.login(input).await.map(|user| user.into()) + } + + // async fn create_invite_code( + // &self, + // ctx: &Context<'_>, + // input: CreateInviteCodeInput, + // ) -> Result { + // let auth_user = get_auth_user(ctx).await?; + // let invite_code_service = ctx.data::()?; + + // let code = invite_code_service + // .create_invite_code(auth_user.id, input) + // .await?; + + // // Get the invite code details to return expires_at + // let invite_codes = invite_code_service + // .get_invite_codes_by_creator(auth_user.id) + // .await?; + // let invite_code = invite_codes + // .into_iter() + // .find(|ic| ic.code == code) + // .ok_or_else(|| async_graphql::Error::new("Failed to retrieve created invite code"))?; + + // Ok(InviteCodeResponse { + // code, + // expires_at: invite_code.expires_at, + // }) + // } +} diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs new file mode 100644 index 0000000..a3c76ff --- /dev/null +++ b/src/graphql/queries.rs @@ -0,0 +1,13 @@ +mod blog; +mod config; +mod permissions; +mod user; +use async_graphql::MergedObject; + +#[derive(MergedObject, Default)] +pub struct QueryRoot( + blog::BlogQuery, + config::ConfigQuery, + permissions::PermissionQuery, + user::UserQuery, +); diff --git a/src/graphql/queries/blog.rs b/src/graphql/queries/blog.rs new file mode 100644 index 0000000..ad8c516 --- /dev/null +++ b/src/graphql/queries/blog.rs @@ -0,0 +1,120 @@ +use crate::graphql::types::{blog::*, PaginatedResult, PaginationInput}; +use crate::services::blog_service::BlogService; +use async_graphql::{Context, Error as GraphQLError, Object, Result}; +use uuid::Uuid; + +#[derive(Default)] +pub struct BlogQuery; + +#[Object] +impl BlogQuery { + async fn blogs( + &self, + ctx: &Context<'_>, + filter: Option, + sort: Option, + pagination: Option, + ) -> Result> { + let blog_service = ctx.data::()?; + let result = blog_service + .get_blogs(filter, sort, pagination) + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + + // 转换 model 类型到 GraphQL 类型 + Ok(PaginatedResult::new( + result.items.into_iter().map(|item| item.into()).collect(), + result.total, + result.page, + result.per_page, + )) + } + + /// 根据ID获取博客文章 + async fn blog(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + let blog = blog_service + .get_blog_by_id(id) + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + Ok(blog.into()) + } + + /// 根据slug获取博客文章 + async fn blog_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result { + let blog_service = ctx.data::()?; + let blog = blog_service + .get_blog_by_slug(&slug) + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + Ok(blog.into()) + } + + async fn blog_detail(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + let detail = blog_service + .get_blog_detail(id) + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + + // 手动转换 BlogDetail,因为它包含嵌套结构 + Ok(BlogDetail { + blog: detail.blog.into(), + category: detail.category.map(|c| c.into()), + tags: detail.tags.into_iter().map(|t| t.into()).collect(), + }) + } + + async fn blog_stats(&self, ctx: &Context<'_>) -> Result { + let blog_service = ctx.data::()?; + let stats = blog_service + .get_blog_stats() + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + Ok(stats.into()) + } + + async fn blog_categories( + &self, + ctx: &Context<'_>, + filter: Option, + ) -> Result> { + let blog_service = ctx.data::()?; + let categories = blog_service + .get_categories(filter) + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + Ok(categories.into_iter().map(|c| c.into()).collect()) + } + + async fn blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + let category = blog_service + .get_category_by_id(id) + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + Ok(category.into()) + } + + async fn blog_tags( + &self, + ctx: &Context<'_>, + filter: Option, + ) -> Result> { + let blog_service = ctx.data::()?; + let tags = blog_service + .get_tags(filter) + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + Ok(tags.into_iter().map(|t| t.into()).collect()) + } + + async fn blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + let tag = blog_service + .get_tag_by_id(id) + .await + .map_err(|e| GraphQLError::new(e.to_string()))?; + Ok(tag.into()) + } +} diff --git a/src/graphql/queries/config.rs b/src/graphql/queries/config.rs new file mode 100644 index 0000000..a293d3b --- /dev/null +++ b/src/graphql/queries/config.rs @@ -0,0 +1,23 @@ +use crate::graphql::guards::*; +use crate::graphql::types::{config::*, permission::*}; +use crate::services::config_manager::ConfigsManager; +use async_graphql::{Context, Object, Result}; + +#[derive(Default)] +pub struct ConfigQuery; + +#[Object] +impl ConfigQuery { + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + async fn configs(&self, ctx: &Context<'_>) -> Result> { + let configs_service = ctx.data::()?; + let configs = configs_service.get_all_settings().await?; + Ok(configs) + } + + async fn site_configs(&self, ctx: &Context<'_>) -> Result> { + let configs_service = ctx.data::()?; + let configs = configs_service.get_settings_by_category("site").await?; + Ok(configs) + } +} diff --git a/src/graphql/queries/permissions.rs b/src/graphql/queries/permissions.rs new file mode 100644 index 0000000..f532ab1 --- /dev/null +++ b/src/graphql/queries/permissions.rs @@ -0,0 +1,132 @@ +use crate::auth::get_auth_user; +use crate::graphql::guards::*; +use crate::graphql::types::*; +use crate::services::casbin_service::CasbinService; +use async_graphql::{Context, Object, Result}; + +#[derive(Default)] +pub struct PermissionQuery; + +#[Object] +impl PermissionQuery { + // 权限管理查询 + #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] + async fn check_permission( + &self, + ctx: &Context<'_>, + resource: String, + action: String, + ) -> Result { + let user = get_auth_user(ctx).await?; + let casbin_service = ctx.data::()?; + + let has_permission = casbin_service + .check_permission(&user.id.to_string(), &resource, &action) + .await?; + + let roles = casbin_service.get_user_roles(&user.id.to_string()).await?; + + Ok(PermissionCheckResult { + user_id: user.id.to_string(), + resource, + action, + has_permission, + roles, + }) + } + + #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] + async fn get_user_roles(&self, ctx: &Context<'_>) -> Result> { + let user = get_auth_user(ctx).await?; + let casbin_service = ctx.data::()?; + + let roles = casbin_service.get_user_roles(&user.id.to_string()).await?; + Ok(roles) + } + + #[graphql(guard = "RequireLogin")] + async fn get_user_permissions(&self, ctx: &Context<'_>) -> Result> { + let user = get_auth_user(ctx).await?; + let casbin_service = ctx.data::()?; + + let permissions = casbin_service + .get_user_permissions(&user.id.to_string()) + .await?; + + Ok(permissions + .into_iter() + .map(|p| PermissionPair { + resource: p.0, + action: p.1, + }) + .collect()) + } + + #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] + async fn get_all_policies(&self, ctx: &Context<'_>) -> Result> { + let casbin_service = ctx.data::()?; + + let policies = casbin_service.get_all_policies().await?; + + Ok(policies + .into_iter() + .filter(|p| p.len() >= 3) + .map(|p| PolicyType { + role: p[0].clone(), + resource: p[1].clone(), + action: p[2].clone(), + }) + .collect()) + } + + #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] + async fn get_role_permissions( + &self, + ctx: &Context<'_>, + role_name: String, + ) -> Result> { + let casbin_service = ctx.data::()?; + + let permissions = casbin_service.get_role_permissions(&role_name).await?; + Ok(permissions + .into_iter() + .map(|p| PermissionPair { + resource: p.0, + action: p.1, + }) + .collect()) + } + + #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] + async fn can_read(&self, ctx: &Context<'_>, resource: String) -> Result { + let user = get_auth_user(ctx).await?; + let casbin_service = ctx.data::()?; + + let can_read = casbin_service + .can_read(&user.id.to_string(), &resource) + .await?; + Ok(can_read) + } + + #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] + async fn can_write(&self, ctx: &Context<'_>, resource: String) -> Result { + let user = get_auth_user(ctx).await?; + let casbin_service = ctx.data::()?; + + let can_write = casbin_service + .can_write(&user.id.to_string(), &resource) + .await?; + Ok(can_write) + } + + #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] + async fn can_delete(&self, ctx: &Context<'_>, resource: String) -> Result { + let user = get_auth_user(ctx).await?; + let casbin_service = ctx.data::()?; + + let can_delete = casbin_service + .can_delete(&user.id.to_string(), &resource) + .await?; + Ok(can_delete) + } +} diff --git a/src/graphql/queries/user.rs b/src/graphql/queries/user.rs new file mode 100644 index 0000000..3b08dd2 --- /dev/null +++ b/src/graphql/queries/user.rs @@ -0,0 +1,166 @@ +use crate::auth::get_auth_user; +use crate::graphql::guards::*; +use crate::graphql::types::users::*; +use crate::services::casbin_service::CasbinService; +use crate::services::user_service::UserService; +use async_graphql::{Context, Object, Result}; +use tracing::info; +use tracing_subscriber::filter; + +#[derive(Default)] +pub struct UserQuery; + +#[Object] +impl UserQuery { + #[graphql(guard = "RequireReadPermission::new(\"users\")")] + async fn current_user(&self, ctx: &Context<'_>) -> Result { + let auth_user = get_auth_user(ctx).await?; + let user_service = ctx.data::()?; + + user_service + .get_user_by_id(auth_user.id) + .await? + .map(|user| user.into()) + .ok_or_else(|| async_graphql::Error::new("User not found")) + } + + /// 获取当前用户的组信息 + #[graphql(guard = "RequireReadPermission::new(\"users\")")] + async fn current_user_groups(&self, ctx: &Context<'_>) -> Result> { + let auth_user = get_auth_user(ctx).await?; + let casbin_service = ctx.data::()?; + + let groups = casbin_service + .get_user_roles(&auth_user.id.to_string()) + .await + .map_err(|e| async_graphql::Error::new(format!("Failed to get user groups: {}", e)))?; + + Ok(groups) + } + + /// 获取当前用户信息(包含组) + #[graphql(guard = "RequireReadPermission::new(\"users\")")] + async fn current_user_with_groups(&self, ctx: &Context<'_>) -> Result { + let auth_user = get_auth_user(ctx).await?; + let user_service = ctx.data::()?; + + let user_with_groups = user_service + .get_user_with_groups(auth_user.id) + .await? + .ok_or_else(|| async_graphql::Error::new("User not found"))?; + + Ok(user_with_groups.into()) + } + + /// 获取指定用户的组信息 + #[graphql(guard = "RequireReadPermission::new(\"users\")")] + async fn user_groups(&self, ctx: &Context<'_>, user_id: uuid::Uuid) -> Result> { + let casbin_service = ctx.data::()?; + + let groups = casbin_service + .get_user_roles(&user_id.to_string()) + .await + .map_err(|e| async_graphql::Error::new(format!("Failed to get user groups: {}", e)))?; + + Ok(groups) + } + + /// 获取指定用户信息(包含组) + #[graphql(guard = "RequireReadPermission::new(\"users\")")] + async fn user_with_groups( + &self, + ctx: &Context<'_>, + user_id: uuid::Uuid, + ) -> Result { + let user_service = ctx.data::()?; + + let user_with_groups = user_service + .get_user_with_groups(user_id) + .await? + .ok_or_else(|| async_graphql::Error::new("User not found"))?; + + Ok(user_with_groups.into()) + } + + #[graphql(guard = "RequireReadPermission::new(\"users\")")] + async fn users( + &self, + ctx: &Context<'_>, + offset: Option, + limit: Option, + sort_by: Option, + sort_order: Option, + filter: Option, + ) -> Result> { + let user_service = ctx.data::()?; + info!("users im here"); + let offset = offset.unwrap_or(0); + let limit = limit.unwrap_or(20); + let sort_by = sort_by.unwrap_or("created_at".to_string()); + let sort_order = sort_order.unwrap_or("desc".to_string()); + user_service + .get_all_users(offset, limit, sort_by, sort_order, filter) + .await + .map(|user| user.into_iter().map(|u| u.into()).collect()) + } + + #[graphql(guard = "RequireReadPermission::new(\"users\")")] + async fn users_with_groups( + &self, + ctx: &Context<'_>, + offset: Option, + limit: Option, + sort_by: Option, + sort_order: Option, + filter: Option, + ) -> Result> { + let user_service = ctx.data::()?; + let offset = offset.unwrap_or(0); + let limit = limit.unwrap_or(20); + let sort_by = sort_by.unwrap_or("created_at".to_string()); + let sort_order = sort_order.unwrap_or("desc".to_string()); + user_service + .get_all_users_with_groups(offset, limit, sort_by, sort_order, filter) + .await + .map(|user| user.into_iter().map(|u| u.into()).collect()) + } + + #[graphql(guard = "RequireReadPermission::new(\"users\")")] + async fn users_info( + &self, + ctx: &Context<'_>, + offset: Option, + limit: Option, + sort_by: Option, + sort_order: Option, + filter: Option, + ) -> Result { + let user_service = ctx.data::()?; + let offset = offset.unwrap_or(0); + let limit = limit.unwrap_or(20); + let sort_by = sort_by.unwrap_or("created_at".to_string()); + let sort_order = sort_order.unwrap_or("desc".to_string()); + + user_service + .users_info(offset, limit, sort_by, sort_order, filter) + .await + .map(|r| r.into()) + } + + // #[graphql(guard = "RequireReadPermission::new(\"invite_codes\")")] + // async fn my_invite_codes(&self, ctx: &Context<'_>) -> Result> { + // let auth_user = get_auth_user(ctx).await?; + // let invite_code_service = ctx.data::()?; + // invite_code_service + // .get_invite_codes_by_creator(auth_user.id) + // .await + // } + + // #[graphql(guard = "RequirePermission::new(\"invite_codes\", \"write\")")] + // async fn validate_invite_code(&self, ctx: &Context<'_>, code: String) -> Result { + // let invite_code_service = ctx.data::()?; + // invite_code_service + // .validate_invite_code(crate::models::invite_code::ValidateInviteCodeInput { code }) + // .await + // } +} diff --git a/src/graphql/query.rs b/src/graphql/query.rs deleted file mode 100644 index fa3de28..0000000 --- a/src/graphql/query.rs +++ /dev/null @@ -1,1336 +0,0 @@ -use crate::auth::get_auth_user; -use crate::graphql::guards::{ - RequireMultiplePermissions, RequirePermission, RequireReadPermission, -}; -use crate::graphql::types::*; -use crate::models::blog::{ - Blog, BlogCategory, BlogCategoryFilterInput, BlogDetail, BlogFilterInput, BlogSortInput, - BlogStats, BlogTag, BlogTagFilterInput, -}; -use crate::models::invite_code::InviteCode; -use crate::models::page_block::{PaginatedResult, PaginationInput}; -use crate::models::settings::SettingFilter; -use crate::models::user::{User, UserInfoRow}; -use crate::services::blog_service::BlogService; -use crate::services::casbin_service::CasbinService; -use crate::services::invite_code_service::InviteCodeService; -use crate::services::page_block_service::PageBlockService; -use crate::services::settings_service::SettingsService; -use crate::services::user_service::UserService; -use async_graphql::Error as GraphQLError; -use async_graphql::{Context, Object, Result}; -use tracing::info; -use uuid::Uuid; - -pub struct QueryRoot; - -#[Object] -impl QueryRoot { - async fn health_check(&self) -> &str { - "OK" - } - - #[graphql(guard = "RequireReadPermission::new(\"users\")")] - async fn current_user(&self, ctx: &Context<'_>) -> Result { - let auth_user = get_auth_user(ctx).await?; - let user_service = ctx.data::()?; - - user_service - .get_user_by_id(auth_user.id) - .await? - .ok_or_else(|| async_graphql::Error::new("User not found")) - } - - #[graphql(guard = "RequireReadPermission::new(\"admin\")")] - async fn secret_data(&self, _ctx: &Context<'_>) -> &str { - "This is super secret admin data!" - } - - #[graphql(guard = "RequireReadPermission::new(\"users\")")] - async fn users( - &self, - ctx: &Context<'_>, - offset: Option, - limit: Option, - sort_by: Option, - sort_order: Option, - filter: Option, - ) -> Result> { - let user_service = ctx.data::()?; - info!("users im here"); - let offset = offset.unwrap_or(0); - let limit = limit.unwrap_or(20); - let sort_by = sort_by.unwrap_or("created_at".to_string()); - let sort_order = sort_order.unwrap_or("desc".to_string()); - user_service - .get_all_users(offset, limit, sort_by, sort_order, filter) - .await - } - - #[graphql(guard = "RequireReadPermission::new(\"invite_codes\")")] - async fn my_invite_codes(&self, ctx: &Context<'_>) -> Result> { - let auth_user = get_auth_user(ctx).await?; - let invite_code_service = ctx.data::()?; - invite_code_service - .get_invite_codes_by_creator(auth_user.id) - .await - } - - #[graphql(guard = "RequirePermission::new(\"invite_codes\", \"write\")")] - async fn validate_invite_code(&self, ctx: &Context<'_>, code: String) -> Result { - let invite_code_service = ctx.data::()?; - invite_code_service - .validate_invite_code(crate::models::invite_code::ValidateInviteCodeInput { code }) - .await - } - - #[graphql(guard = "RequireReadPermission::new(\"users\")")] - async fn users_info( - &self, - ctx: &Context<'_>, - offset: Option, - limit: Option, - sort_by: Option, - sort_order: Option, - filter: Option, - ) -> Result { - let user_service = ctx.data::()?; - let offset = offset.unwrap_or(0); - let limit = limit.unwrap_or(20); - let sort_by = sort_by.unwrap_or("created_at".to_string()); - let sort_order = sort_order.unwrap_or("desc".to_string()); - - user_service - .users_info(offset, limit, sort_by, sort_order, filter) - .await - } - - // Settings queries - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn settings( - &self, - ctx: &Context<'_>, - filter: Option, - ) -> Result> { - let settings_service = ctx.data::()?; - let filter = filter.map(|f| SettingFilter { - category: f.category, - is_system: f.is_system, - is_editable: f.is_editable, - search: f.search, - }); - - let settings = if let Some(filter) = filter { - settings_service.get_settings_with_filter(&filter).await? - } else { - settings_service.get_all_settings().await? - }; - - Ok(settings - .into_iter() - .map(|s| SettingType { - id: s.id, - key: s.key, - value: s.value, - value_type: s.value_type, - description: s.description, - category: s.category, - is_encrypted: s.is_encrypted, - is_system: s.is_system, - is_editable: s.is_editable, - created_at: s.created_at, - updated_at: s.updated_at, - created_by: s.created_by, - updated_by: s.updated_by, - }) - .collect()) - } - - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn setting_by_key(&self, ctx: &Context<'_>, key: String) -> Result> { - let settings_service = ctx.data::()?; - let setting = settings_service.get_setting_by_key(&key).await?; - - Ok(setting.map(|s| SettingType { - id: s.id, - key: s.key, - value: s.value, - value_type: s.value_type, - description: s.description, - category: s.category, - is_encrypted: s.is_encrypted, - is_system: s.is_system, - is_editable: s.is_editable, - created_at: s.created_at, - updated_at: s.updated_at, - created_by: s.created_by, - updated_by: s.updated_by, - })) - } - - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn setting_by_id(&self, ctx: &Context<'_>, id: Uuid) -> Result> { - let settings_service = ctx.data::()?; - let setting = settings_service.get_setting_by_id(id).await?; - - Ok(setting.map(|s| SettingType { - id: s.id, - key: s.key, - value: s.value, - value_type: s.value_type, - description: s.description, - category: s.category, - is_encrypted: s.is_encrypted, - is_system: s.is_system, - is_editable: s.is_editable, - created_at: s.created_at, - updated_at: s.updated_at, - created_by: s.created_by, - updated_by: s.updated_by, - })) - } - - #[graphql( - guard = "RequireMultiplePermissions::new(&[(\"settings\", \"read\"), (\"pages\", \"read\")])" - )] - async fn setting_categories(&self, ctx: &Context<'_>) -> Result> { - let page_block_service = ctx.data::()?; - let settings_service = ctx.data::()?; - - // 获取所有配置分类 - let categories = settings_service.get_categories().await?; - - let mut category_pages = Vec::new(); - - // 为每个分类创建 CategoryPageType - for category in categories { - // 获取页面和统计信息 - let (page, total_count, system_count, editable_count) = page_block_service - .get_category_page_with_stats(&category) - .await?; - - // 获取该分类下的所有配置项 - let filter = SettingFilter { - category: Some(category.clone()), - is_system: None, - is_editable: None, - search: None, - }; - let settings = settings_service.get_settings_with_filter(&filter).await?; - - // 转换为 GraphQL 类型 - let settings_types = settings - .into_iter() - .map(|s| SettingCenterType { - id: s.id, - key: s.key, - value: if s.is_encrypted.unwrap_or(false) { - Some("***".to_string()) // 占位符,不返回明文 - } else { - s.value - }, - value_type: s.value_type, - is_encrypted: s.is_encrypted, - is_editable: s.is_editable, - is_system: s.is_system, - description: s.description, - updated_at: s.updated_at, - }) - .collect(); - - // 转换页面类型 - let page_type = page.map(|p| PageType { - id: p.id, - title: p.title, - slug: p.slug, - description: p.description, - is_active: p.is_active, - created_at: p.created_at, - updated_at: p.updated_at, - created_by: p.created_by, - updated_by: p.updated_by, - }); - - category_pages.push(CategoryPageType { - page: page_type, - settings: settings_types, - category, - settings_count: total_count, - system_settings_count: system_count, - editable_settings_count: editable_count, - }); - } - - Ok(category_pages) - } - - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn settings_stats(&self, ctx: &Context<'_>) -> Result { - let settings_service = ctx.data::()?; - let categories = settings_service.get_categories().await?; - let stats = settings_service.get_settings_stats().await?; - - Ok(SettingsStatsType { categories, stats }) - } - - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn setting_history( - &self, - ctx: &Context<'_>, - setting_id: Uuid, - ) -> Result> { - let settings_service = ctx.data::()?; - let history = settings_service.get_setting_history(setting_id).await?; - - Ok(history - .into_iter() - .map(|h| SettingHistoryType { - id: h.id, - setting_id: h.setting_id, - old_value: h.old_value, - new_value: h.new_value, - changed_by: h.changed_by, - change_reason: h.change_reason, - created_at: h.created_at, - }) - .collect()) - } - - // Page Block queries - #[graphql(guard = "RequireReadPermission::new(\"pages\")")] - async fn pages( - &self, - ctx: &Context<'_>, - filter: Option, - limit: Option, - offset: Option, - ) -> Result> { - let page_block_service = ctx.data::()?; - let filter = filter.map(|f| crate::models::page_block::PageFilterInput { - title: f.title, - slug: f.slug, - is_active: f.is_active, - search: f.search, - }); - - let pages = page_block_service.get_pages(filter, limit, offset).await?; - - Ok(pages - .into_iter() - .map(|p| PageType { - id: p.id, - title: p.title, - slug: p.slug, - description: p.description, - is_active: p.is_active, - created_at: p.created_at, - updated_at: p.updated_at, - created_by: p.created_by, - updated_by: p.updated_by, - }) - .collect()) - } - - #[graphql(guard = "RequireReadPermission::new(\"pages\")")] - async fn page_by_id(&self, ctx: &Context<'_>, id: Uuid) -> Result> { - let page_block_service = ctx.data::()?; - - match page_block_service.get_page_by_id(id).await { - Ok(page) => Ok(Some(PageType { - id: page.id, - title: page.title, - slug: page.slug, - description: page.description, - is_active: page.is_active, - created_at: page.created_at, - updated_at: page.updated_at, - created_by: page.created_by, - updated_by: page.updated_by, - })), - Err(_) => Ok(None), - } - } - - #[graphql(guard = "RequireReadPermission::new(\"pages\")")] - async fn page_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result> { - let page_block_service = ctx.data::()?; - - match page_block_service.get_page_by_slug(&slug).await { - Ok(page) => Ok(Some(PageType { - id: page.id, - title: page.title, - slug: page.slug, - description: page.description, - is_active: page.is_active, - created_at: page.created_at, - updated_at: page.updated_at, - created_by: page.created_by, - updated_by: page.updated_by, - })), - Err(_) => Ok(None), - } - } - - #[graphql(guard = "RequireReadPermission::new(\"page_blocks\")")] - async fn page_blocks(&self, ctx: &Context<'_>, page_id: Uuid) -> Result> { - let page_block_service = ctx.data::()?; - let blocks = page_block_service.get_page_blocks(page_id).await?; - - Ok(blocks - .into_iter() - .map(|block| match block { - crate::models::page_block::Block::TextBlock(tb) => { - BlockType::TextBlock(TextBlockType { - id: tb.id, - page_id: tb.page_id, - block_order: tb.block_order, - title: tb.title, - markdown: tb.markdown, - is_active: tb.is_active, - created_at: tb.created_at, - updated_at: tb.updated_at, - }) - } - crate::models::page_block::Block::ChartBlock(cb) => { - BlockType::ChartBlock(ChartBlockType { - id: cb.id, - page_id: cb.page_id, - block_order: cb.block_order, - title: cb.title, - chart_type: cb.chart_type, - series: cb - .series - .into_iter() - .map(|dp| DataPointType { - id: dp.id, - chart_block_id: dp.chart_block_id, - x: dp.x, - y: dp.y, - label: dp.label, - color: dp.color, - }) - .collect(), - config: cb.config, - is_active: cb.is_active, - created_at: cb.created_at, - updated_at: cb.updated_at, - }) - } - crate::models::page_block::Block::SettingsBlock(sb) => { - BlockType::SettingsBlock(SettingsBlockType { - id: sb.id, - page_id: sb.page_id, - block_order: sb.block_order, - title: sb.title, - category: sb.category, - editable: sb.editable, - display_mode: sb.display_mode, - is_active: sb.is_active, - created_at: sb.created_at, - updated_at: sb.updated_at, - }) - } - crate::models::page_block::Block::TableBlock(tb) => { - BlockType::TableBlock(TableBlockType { - id: tb.id, - page_id: tb.page_id, - block_order: tb.block_order, - title: tb.title, - columns: tb - .columns - .into_iter() - .map(|col| TableColumnType { - id: col.id, - table_block_id: col.table_block_id, - name: col.name, - label: col.label, - data_type: col.data_type, - is_sortable: col.is_sortable, - is_filterable: col.is_filterable, - width: col.width, - order: col.order, - }) - .collect(), - data_source: tb.data_source, - data_config: tb.data_config, - is_active: tb.is_active, - created_at: tb.created_at, - updated_at: tb.updated_at, - }) - } - crate::models::page_block::Block::HeroBlock(hb) => { - BlockType::HeroBlock(HeroBlockType { - id: hb.id, - page_id: hb.page_id, - block_order: hb.block_order, - title: hb.title, - subtitle: hb.subtitle, - background_image: hb.background_image, - background_color: hb.background_color, - text_color: hb.text_color, - cta_text: hb.cta_text, - cta_link: hb.cta_link, - is_active: hb.is_active, - created_at: hb.created_at, - updated_at: hb.updated_at, - }) - } - }) - .collect()) - } - - /// 根据配置分类获取对应的页面 - #[graphql( - guard = "RequireMultiplePermissions::new(&[(\"pages\", \"read\"), (\"settings\", \"read\")])" - )] - async fn page_by_category( - &self, - ctx: &Context<'_>, - category: String, - ) -> Result { - let page_block_service = ctx.data::()?; - let settings_service = ctx.data::()?; - - // 获取页面和统计信息 - let (page, total_count, system_count, editable_count) = page_block_service - .get_category_page_with_stats(&category) - .await?; - - // 获取该分类下的所有配置项 - let filter = SettingFilter { - category: Some(category.clone()), - is_system: None, - is_editable: None, - search: None, - }; - let settings = settings_service.get_settings_with_filter(&filter).await?; - - // 转换为 GraphQL 类型 - let settings_types = settings - .into_iter() - .map(|s| SettingCenterType { - id: s.id, - key: s.key, - value: if s.is_encrypted.unwrap_or(false) { - Some("***".to_string()) // 占位符,不返回明文 - } else { - s.value - }, - value_type: s.value_type, - is_encrypted: s.is_encrypted, - is_editable: s.is_editable, - is_system: s.is_system, - description: s.description, - updated_at: s.updated_at, - }) - .collect(); - - // 转换页面类型 - let page_type = page.map(|p| PageType { - id: p.id, - title: p.title, - slug: p.slug, - description: p.description, - is_active: p.is_active, - created_at: p.created_at, - updated_at: p.updated_at, - created_by: p.created_by, - updated_by: p.updated_by, - }); - - Ok(CategoryPageType { - page: page_type, - settings: settings_types, - category, - settings_count: total_count, - system_settings_count: system_count, - editable_settings_count: editable_count, - }) - } - - /// 获取所有配置分类页面 - #[graphql(guard = "RequireReadPermission::new(\"pages\")")] - async fn all_category_pages(&self, ctx: &Context<'_>) -> Result> { - let page_block_service = ctx.data::()?; - let pages = page_block_service.get_all_category_pages().await?; - - Ok(pages - .into_iter() - .map(|p| PageType { - id: p.id, - title: p.title, - slug: p.slug, - description: p.description, - is_active: p.is_active, - created_at: p.created_at, - updated_at: p.updated_at, - created_by: p.created_by, - updated_by: p.updated_by, - }) - .collect()) - } - - // Enhanced Settings queries for settings center - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn settings_by_category( - &self, - ctx: &Context<'_>, - category: String, - ) -> Result> { - let settings_service = ctx.data::()?; - let filter = SettingFilter { - category: Some(category), - is_system: None, - is_editable: None, - search: None, - }; - - let settings = settings_service.get_settings_with_filter(&filter).await?; - - Ok(settings - .into_iter() - .map(|s| SettingCenterType { - id: s.id, - key: s.key, - value: if s.is_encrypted.unwrap_or(false) { - Some("***".to_string()) // 占位符,不返回明文 - } else { - s.value - }, - value_type: s.value_type, - is_encrypted: s.is_encrypted, - is_editable: s.is_editable, - is_system: s.is_system, - description: s.description, - updated_at: s.updated_at, - }) - .collect()) - } - - // 权限管理查询 - #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] - async fn check_permission( - &self, - ctx: &Context<'_>, - resource: String, - action: String, - ) -> Result { - let user = get_auth_user(ctx).await?; - let casbin_service = ctx.data::()?; - - let has_permission = casbin_service - .check_permission(&user.id.to_string(), &resource, &action) - .await?; - - let roles = casbin_service.get_user_roles(&user.id.to_string()).await?; - - Ok(PermissionCheckResult { - user_id: user.id.to_string(), - resource, - action, - has_permission, - roles, - }) - } - - #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] - async fn get_user_roles(&self, ctx: &Context<'_>) -> Result> { - let user = get_auth_user(ctx).await?; - let casbin_service = ctx.data::()?; - - let roles = casbin_service.get_user_roles(&user.id.to_string()).await?; - Ok(roles) - } - - #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] - async fn get_all_policies(&self, ctx: &Context<'_>) -> Result> { - let casbin_service = ctx.data::()?; - - let policies = casbin_service.get_all_policies().await?; - - Ok(policies - .into_iter() - .filter(|p| p.len() >= 3) - .map(|p| PolicyType { - role: p[0].clone(), - resource: p[1].clone(), - action: p[2].clone(), - }) - .collect()) - } - - #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] - async fn get_role_permissions( - &self, - ctx: &Context<'_>, - role_name: String, - ) -> Result> { - let casbin_service = ctx.data::()?; - - let permissions = casbin_service.get_role_permissions(&role_name).await?; - Ok(permissions - .into_iter() - .map(|p| PermissionPair { - resource: p.0, - action: p.1, - }) - .collect()) - } - - #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] - async fn can_read(&self, ctx: &Context<'_>, resource: String) -> Result { - let user = get_auth_user(ctx).await?; - let casbin_service = ctx.data::()?; - - let can_read = casbin_service - .can_read(&user.id.to_string(), &resource) - .await?; - Ok(can_read) - } - - #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] - async fn can_write(&self, ctx: &Context<'_>, resource: String) -> Result { - let user = get_auth_user(ctx).await?; - let casbin_service = ctx.data::()?; - - let can_write = casbin_service - .can_write(&user.id.to_string(), &resource) - .await?; - Ok(can_write) - } - - #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] - async fn can_delete(&self, ctx: &Context<'_>, resource: String) -> Result { - let user = get_auth_user(ctx).await?; - let casbin_service = ctx.data::()?; - - let can_delete = casbin_service - .can_delete(&user.id.to_string(), &resource) - .await?; - Ok(can_delete) - } - - // 站点与运营配置查询方法 - async fn site_ops_config(&self, ctx: &Context<'_>) -> Result { - let settings_service = ctx.data::()?; - - // 获取站点配置 - let site_name = settings_service - .get_setting_by_key("site.name") - .await? - .and_then(|s| s.get_string().ok()) - .unwrap_or_else(|| "MMAP System".to_string()); - - let locale_default = settings_service - .get_setting_by_key("site.locale_default") - .await? - .and_then(|s| s.get_string().ok()) - .unwrap_or_else(|| "zh-CN".to_string()); - - let locales_supported = settings_service - .get_setting_by_key("site.locales_supported") - .await? - .and_then(|s| s.get_json().ok()) - .and_then(|v| v.as_array().cloned()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_else(|| vec!["zh-CN".to_string(), "en".to_string()]); - - // 获取品牌配置 - let logo_url = settings_service - .get_setting_by_key("site.brand.logo_url") - .await? - .and_then(|s| s.get_string().ok()) - .unwrap_or_else(|| "/images/logo.png".to_string()); - - let primary_color = settings_service - .get_setting_by_key("site.brand.primary_color") - .await? - .and_then(|s| s.get_string().ok()) - .unwrap_or_else(|| "#3B82F6".to_string()); - - let dark_mode_default = settings_service - .get_setting_by_key("site.brand.dark_mode_default") - .await? - .and_then(|s| s.get_bool().ok()) - .unwrap_or(false); - - // 获取页脚链接 - let footer_links = settings_service - .get_setting_by_key("site.footer_links") - .await? - .and_then(|s| s.get_json().ok()) - .and_then(|v| v.as_array().cloned()) - .map(|arr| { - arr.iter() - .filter_map(|v| { - if let (Some(name), Some(url), Some(visible)) = ( - v.get("name").and_then(|n| n.as_str()), - v.get("url").and_then(|u| u.as_str()), - v.get("visible_to_guest").and_then(|vis| vis.as_bool()), - ) { - Some(FooterLinkType { - name: name.to_string(), - url: url.to_string(), - visible_to_guest: visible, - }) - } else { - None - } - }) - .collect() - }) - .unwrap_or_else(|| { - vec![ - FooterLinkType { - name: "关于我们".to_string(), - url: "/about".to_string(), - visible_to_guest: true, - }, - FooterLinkType { - name: "联系我们".to_string(), - url: "/contact".to_string(), - visible_to_guest: true, - }, - ] - }); - - // 获取横幅公告配置 - let banner_enabled = settings_service - .get_setting_by_key("notice.banner.enabled") - .await? - .and_then(|s| s.get_bool().ok()) - .unwrap_or(false); - - let banner_text = settings_service - .get_setting_by_key("notice.banner.text") - .await? - .and_then(|s| s.get_json().ok()) - .and_then(|v| v.as_object().cloned()) - .map(|obj| { - obj.iter() - .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) - .collect() - }) - .unwrap_or_else(|| { - let mut map = std::collections::HashMap::new(); - map.insert("zh-CN".to_string(), "欢迎使用MMAP系统".to_string()); - map.insert("en".to_string(), "Welcome to MMAP System".to_string()); - map - }); - - // 获取维护窗口配置 - let maintenance_config = settings_service.get_setting_by_key("maintenance.window").await? - .and_then(|s| s.get_json().ok()) - .unwrap_or_else(|| serde_json::json!({ - "enabled": false, - "start_time": null, - "end_time": null, - "message": {"zh-CN": "系统维护中,请稍后再试", "en": "System maintenance in progress"} - })); - - let maintenance_enabled = maintenance_config - .get("enabled") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let maintenance_start_time = maintenance_config - .get("start_time") - .and_then(|v| v.as_str()) - .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) - .map(|dt| dt.with_timezone(&chrono::Utc)); - - let maintenance_end_time = maintenance_config - .get("end_time") - .and_then(|v| v.as_str()) - .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) - .map(|dt| dt.with_timezone(&chrono::Utc)); - - let maintenance_message = maintenance_config - .get("message") - .and_then(|v| v.as_object()) - .map(|obj| { - obj.iter() - .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) - .collect() - }) - .unwrap_or_else(|| { - let mut map = std::collections::HashMap::new(); - map.insert("zh-CN".to_string(), "系统维护中,请稍后再试".to_string()); - map.insert( - "en".to_string(), - "System maintenance in progress".to_string(), - ); - map - }); - - // 获取弹窗公告 - let modal_announcements = settings_service - .get_setting_by_key("modal.announcements") - .await? - .and_then(|s| s.get_json().ok()) - .and_then(|v| v.as_array().cloned()) - .map(|arr| { - arr.iter() - .filter_map(|v| { - if let ( - Some(id), - Some(title), - Some(content), - Some(start_time), - Some(end_time), - Some(audience), - Some(priority), - ) = ( - v.get("id").and_then(|id| id.as_str()), - v.get("title").and_then(|t| t.as_object()), - v.get("content").and_then(|c| c.as_object()), - v.get("start_time").and_then(|st| st.as_str()), - v.get("end_time").and_then(|et| et.as_str()), - v.get("audience").and_then(|a| a.as_array()), - v.get("priority").and_then(|p| p.as_str()), - ) { - let title_map: std::collections::HashMap = title - .iter() - .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) - .collect(); - - let content_map: std::collections::HashMap = content - .iter() - .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) - .collect(); - - let audience_vec: Vec = audience - .iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(); - - if let (Ok(start_dt), Ok(end_dt)) = ( - chrono::DateTime::parse_from_rfc3339(start_time), - chrono::DateTime::parse_from_rfc3339(end_time), - ) { - Some(ModalAnnouncementType { - id: id.to_string(), - title: title_map, - content: content_map, - start_time: start_dt.with_timezone(&chrono::Utc), - end_time: end_dt.with_timezone(&chrono::Utc), - audience: audience_vec, - priority: priority.to_string(), - }) - } else { - None - } - } else { - None - } - }) - .collect() - }) - .unwrap_or_default(); - - // 获取文档链接 - let docs_links = settings_service - .get_setting_by_key("docs.links") - .await? - .and_then(|s| s.get_json().ok()) - .and_then(|v| v.as_array().cloned()) - .map(|arr| { - arr.iter() - .filter_map(|v| { - if let (Some(name), Some(url), Some(description)) = ( - v.get("name").and_then(|n| n.as_str()), - v.get("url").and_then(|u| u.as_str()), - v.get("description").and_then(|d| d.as_str()), - ) { - Some(DocLinkType { - name: name.to_string(), - url: url.to_string(), - description: description.to_string(), - }) - } else { - None - } - }) - .collect() - }) - .unwrap_or_default(); - - // 获取支持渠道 - let support_channels = settings_service - .get_setting_by_key("support.channels") - .await? - .and_then(|s| s.get_json().ok()) - .unwrap_or_else(|| { - serde_json::json!({ - "email": "support@mapp.com", - "ticket_system": "/support/tickets", - "chat_groups": [], - "working_hours": {"zh-CN": "周一至周五 9:00-18:00", "en": "Mon-Fri 9:00-18:00"} - }) - }); - - let support_email = support_channels - .get("email") - .and_then(|v| v.as_str()) - .unwrap_or("support@mapp.com") - .to_string(); - - let ticket_system = support_channels - .get("ticket_system") - .and_then(|v| v.as_str()) - .unwrap_or("/support/tickets") - .to_string(); - - let chat_groups = support_channels - .get("chat_groups") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| { - if let (Some(name), Some(description)) = ( - v.get("name").and_then(|n| n.as_str()), - v.get("description").and_then(|d| d.as_str()), - ) { - let url = v.get("url").and_then(|u| u.as_str()).map(|s| s.to_string()); - let qr_code = v - .get("qr_code") - .and_then(|q| q.as_str()) - .map(|s| s.to_string()); - - Some(ChatGroupType { - name: name.to_string(), - url, - qr_code, - description: description.to_string(), - }) - } else { - None - } - }) - .collect() - }) - .unwrap_or_default(); - - let working_hours = support_channels - .get("working_hours") - .and_then(|v| v.as_object()) - .map(|obj| { - obj.iter() - .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) - .collect() - }) - .unwrap_or_else(|| { - let mut map = std::collections::HashMap::new(); - map.insert("zh-CN".to_string(), "周一至周五 9:00-18:00".to_string()); - map.insert("en".to_string(), "Mon-Fri 9:00-18:00".to_string()); - map - }); - - // 获取运营配置 - let ops_features = settings_service - .get_setting_by_key("ops.features.registration_enabled") - .await? - .and_then(|s| s.get_bool().ok()) - .unwrap_or(true); - - let invite_code_required = settings_service - .get_setting_by_key("ops.features.invite_code_required") - .await? - .and_then(|s| s.get_bool().ok()) - .unwrap_or(true); - - let email_verification = settings_service - .get_setting_by_key("ops.features.email_verification") - .await? - .and_then(|s| s.get_bool().ok()) - .unwrap_or(false); - - let max_users = settings_service - .get_setting_by_key("ops.limits.max_users") - .await? - .and_then(|s| s.get_number().ok()) - .map(|n| n as i32) - .unwrap_or(1000); - - let max_invite_codes = settings_service - .get_setting_by_key("ops.limits.max_invite_codes_per_user") - .await? - .and_then(|s| s.get_number().ok()) - .map(|n| n as i32) - .unwrap_or(10); - - let session_timeout = settings_service - .get_setting_by_key("ops.limits.session_timeout_hours") - .await? - .and_then(|s| s.get_number().ok()) - .map(|n| n as i32) - .unwrap_or(24); - - let welcome_email = settings_service - .get_setting_by_key("ops.notifications.welcome_email") - .await? - .and_then(|s| s.get_bool().ok()) - .unwrap_or(true); - - let system_announcements = settings_service - .get_setting_by_key("ops.notifications.system_announcements") - .await? - .and_then(|s| s.get_bool().ok()) - .unwrap_or(true); - - let maintenance_alerts = settings_service - .get_setting_by_key("ops.notifications.maintenance_alerts") - .await? - .and_then(|s| s.get_bool().ok()) - .unwrap_or(true); - - Ok(SiteOpsConfigType { - site: SiteConfigType { - info: SiteInfoType { - name: site_name, - locale_default, - locales_supported, - }, - brand: BrandConfigType { - logo_url, - primary_color, - dark_mode_default, - }, - footer_links, - }, - notice_maintenance: NoticeMaintenanceType { - banner: BannerNoticeType { - enabled: banner_enabled, - text: banner_text, - }, - maintenance_window: MaintenanceWindowType { - enabled: maintenance_enabled, - start_time: maintenance_start_time, - end_time: maintenance_end_time, - message: maintenance_message, - }, - modal_announcements, - }, - docs_support: DocsSupportType { - links: docs_links, - channels: SupportChannelsType { - email: support_email, - ticket_system, - chat_groups, - working_hours, - }, - }, - ops: OpsConfigType { - features: FeatureSwitchesType { - registration_enabled: ops_features, - invite_code_required, - email_verification, - }, - limits: LimitsConfigType { - max_users, - max_invite_codes_per_user: max_invite_codes, - session_timeout_hours: session_timeout, - }, - notifications: NotificationConfigType { - welcome_email, - system_announcements, - maintenance_alerts, - }, - }, - }) - } - - /// 获取站点配置 - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn site_config(&self, ctx: &Context<'_>) -> Result { - let full_config = self.site_ops_config(ctx).await?; - Ok(full_config.site) - } - - /// 获取公告维护配置 - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn notice_maintenance_config(&self, ctx: &Context<'_>) -> Result { - let full_config = self.site_ops_config(ctx).await?; - Ok(full_config.notice_maintenance) - } - - /// 获取文档支持配置 - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn docs_support_config(&self, ctx: &Context<'_>) -> Result { - let full_config = self.site_ops_config(ctx).await?; - Ok(full_config.docs_support) - } - - /// 获取运营配置 - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn ops_config(&self, ctx: &Context<'_>) -> Result { - let full_config = self.site_ops_config(ctx).await?; - Ok(full_config.ops) - } - - /// 验证配置有效性 - #[graphql(guard = "RequireReadPermission::new(\"settings\")")] - async fn validate_config(&self, ctx: &Context<'_>) -> Result { - let settings_service = ctx.data::()?; - let mut errors = Vec::new(); - let mut warnings = Vec::new(); - - // 验证站点名称 - if let Some(setting) = settings_service.get_setting_by_key("site.name").await? { - if let Ok(name) = setting.get_string() { - if name.trim().is_empty() { - errors.push("站点名称不能为空".to_string()); - } - } - } - - // 验证默认语言 - if let Some(setting) = settings_service - .get_setting_by_key("site.locale_default") - .await? - { - if let Ok(locale) = setting.get_string() { - if !["zh-CN", "en"].contains(&locale.as_str()) { - errors.push("默认语言必须是 zh-CN 或 en".to_string()); - } - } - } - - // 验证主题色格式 - if let Some(setting) = settings_service - .get_setting_by_key("site.brand.primary_color") - .await? - { - if let Ok(color) = setting.get_string() { - if !color.starts_with('#') || color.len() != 7 { - warnings.push("主题色格式建议使用 #RRGGBB 格式".to_string()); - } - } - } - - // 验证维护窗口时间 - if let Some(setting) = settings_service - .get_setting_by_key("maintenance.window") - .await? - { - if let Ok(config) = setting.get_json() { - if let (Some(enabled), Some(start), Some(end)) = ( - config.get("enabled").and_then(|v| v.as_bool()), - config.get("start_time").and_then(|v| v.as_str()), - config.get("end_time").and_then(|v| v.as_str()), - ) { - if enabled { - if let (Ok(start_dt), Ok(end_dt)) = ( - chrono::DateTime::parse_from_rfc3339(start), - chrono::DateTime::parse_from_rfc3339(end), - ) { - if start_dt >= end_dt { - errors.push("维护开始时间必须早于结束时间".to_string()); - } - } - } - } - } - } - - Ok(ConfigValidationResultType { - valid: errors.is_empty(), - errors, - warnings, - }) - } - - // ==================== Blog 相关查询 ==================== - - async fn blogs( - &self, - ctx: &Context<'_>, - filter: Option, - sort: Option, - pagination: Option, - ) -> Result> { - let blog_service = ctx.data::()?; - blog_service - .get_blogs(filter, sort, pagination) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 根据ID获取博客文章 - async fn blog(&self, ctx: &Context<'_>, id: Uuid) -> Result { - let blog_service = ctx.data::()?; - blog_service - .get_blog_by_id(id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - /// 根据slug获取博客文章 - async fn blog_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result { - let blog_service = ctx.data::()?; - blog_service - .get_blog_by_slug(&slug) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - async fn blog_detail(&self, ctx: &Context<'_>, id: Uuid) -> Result { - let blog_service = ctx.data::()?; - blog_service - .get_blog_detail(id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - async fn blog_stats(&self, ctx: &Context<'_>) -> Result { - let blog_service = ctx.data::()?; - blog_service - .get_blog_stats() - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - async fn blog_categories( - &self, - ctx: &Context<'_>, - filter: Option, - ) -> Result> { - let blog_service = ctx.data::()?; - blog_service - .get_categories(filter) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - async fn blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result { - let blog_service = ctx.data::()?; - blog_service - .get_category_by_id(id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - async fn blog_tags( - &self, - ctx: &Context<'_>, - filter: Option, - ) -> Result> { - let blog_service = ctx.data::()?; - blog_service - .get_tags(filter) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } - - async fn blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result { - let blog_service = ctx.data::()?; - blog_service - .get_tag_by_id(id) - .await - .map_err(|e| GraphQLError::new(e.to_string())) - } -} diff --git a/src/graphql/types.rs b/src/graphql/types.rs deleted file mode 100644 index 799dc0a..0000000 --- a/src/graphql/types.rs +++ /dev/null @@ -1,701 +0,0 @@ -use crate::models::{user::Role, Setting}; -use async_graphql::{InputObject, SimpleObject, Union}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use uuid::Uuid; - -#[derive(InputObject)] -pub struct RegisterInput { - pub username: String, - pub email: String, - pub password: String, - pub invite_code: String, - pub role: Option, -} - -#[derive(InputObject)] -pub struct CreateUserInput { - pub username: String, - pub email: String, - pub password: String, - pub role: Option, -} - -#[derive(InputObject)] -pub struct LoginInput { - pub username: String, - pub password: String, -} - -#[derive(SimpleObject)] -pub struct LoginResponse { - pub token: String, - pub user_id: String, -} - -#[derive(InputObject)] -pub struct CreateInviteCodeInput { - pub expires_in_days: Option, -} - -#[derive(InputObject)] -pub struct ValidateInviteCodeInput { - pub code: String, -} - -#[derive(SimpleObject)] -pub struct InviteCodeResponse { - pub code: String, - pub expires_at: Option>, -} - -#[derive(InputObject)] -pub struct InitializeAdminInput { - pub username: String, - pub email: String, - pub password: String, -} - -#[derive(SimpleObject)] -pub struct InitializeAdminResponse { - pub success: bool, - pub message: String, - pub user: Option, -} - -#[derive(SimpleObject)] -pub struct UserInfoRespnose { - pub total_users: i64, - pub total_active_users: i64, - pub total_inactive_users: i64, - pub total_admin_users: i64, - pub total_user_users: i64, - - pub users: Vec, -} - -// Settings GraphQL types -#[derive(SimpleObject, Debug, Clone)] -pub struct SettingType { - pub id: Uuid, - pub key: String, - pub value: Option, - pub value_type: String, - pub description: Option, - pub category: Option, - pub is_encrypted: Option, - pub is_system: Option, - pub is_editable: Option, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, - pub created_by: Option, - pub updated_by: Option, -} - -impl From for SettingType { - fn from(setting: Setting) -> Self { - SettingType { - id: setting.id, - key: setting.key, - value: setting.value, - value_type: setting.value_type, - description: setting.description, - category: setting.category, - is_encrypted: setting.is_encrypted, - is_system: setting.is_system, - is_editable: setting.is_editable, - created_at: setting.created_at, - updated_at: setting.updated_at, - created_by: setting.created_by, - updated_by: setting.updated_by, - } - } -} - -#[derive(InputObject)] -pub struct CreateSettingInput { - pub key: String, - pub value: Option, - pub value_type: String, - pub description: Option, - pub category: String, - pub is_encrypted: Option, - pub is_system: Option, - pub is_editable: Option, -} - -#[derive(InputObject)] -pub struct UpdateSettingInput { - pub value: Option, - pub description: Option, - pub category: Option, - pub is_editable: Option, -} - -#[derive(InputObject)] -pub struct SettingFilterInput { - pub category: Option, - pub is_system: Option, - pub is_editable: Option, - pub search: Option, -} - -#[derive(SimpleObject)] -pub struct SettingHistoryType { - pub id: Uuid, - pub setting_id: Uuid, - pub old_value: Option, - pub new_value: Option, - pub changed_by: Option, - pub change_reason: Option, - pub created_at: chrono::DateTime, -} - -#[derive(SimpleObject)] -pub struct SettingsStatsType { - pub categories: Vec, - pub stats: std::collections::HashMap, -} - -// Page Block GraphQL types -#[derive(SimpleObject)] -pub struct PageType { - pub id: Uuid, - pub title: String, - pub slug: String, - pub description: Option, - pub is_active: bool, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, - pub created_by: Option, - pub updated_by: Option, -} - -#[derive(Union)] -pub enum BlockType { - TextBlock(TextBlockType), - ChartBlock(ChartBlockType), - SettingsBlock(SettingsBlockType), - TableBlock(TableBlockType), - HeroBlock(HeroBlockType), -} - -#[derive(SimpleObject)] -pub struct TextBlockType { - pub id: Uuid, - pub page_id: Uuid, - pub block_order: i32, - pub title: Option, - pub markdown: String, - pub is_active: bool, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - -#[derive(SimpleObject)] -pub struct ChartBlockType { - pub id: Uuid, - pub page_id: Uuid, - pub block_order: i32, - pub title: String, - pub chart_type: String, - pub series: Vec, - pub config: Option, - pub is_active: bool, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - -#[derive(SimpleObject)] -pub struct DataPointType { - pub id: Uuid, - pub chart_block_id: Uuid, - pub x: f64, - pub y: f64, - pub label: Option, - pub color: Option, -} - -#[derive(SimpleObject)] -pub struct SettingsBlockType { - pub id: Uuid, - pub page_id: Uuid, - pub block_order: i32, - pub title: Option, - pub category: String, - pub editable: bool, - pub display_mode: String, - pub is_active: bool, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - -#[derive(SimpleObject)] -pub struct TableBlockType { - pub id: Uuid, - pub page_id: Uuid, - pub block_order: i32, - pub title: Option, - pub columns: Vec, - pub data_source: String, - pub data_config: Option, - pub is_active: bool, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - -#[derive(SimpleObject)] -pub struct TableColumnType { - pub id: Uuid, - pub table_block_id: Uuid, - pub name: String, - pub label: String, - pub data_type: String, - pub is_sortable: bool, - pub is_filterable: bool, - pub width: Option, - pub order: i32, -} - -#[derive(SimpleObject)] -pub struct HeroBlockType { - pub id: Uuid, - pub page_id: Uuid, - pub block_order: i32, - pub title: String, - pub subtitle: Option, - pub background_image: Option, - pub background_color: Option, - pub text_color: Option, - pub cta_text: Option, - pub cta_link: Option, - pub is_active: bool, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - -// Page Block Input types -#[derive(InputObject)] -pub struct CreatePageInputType { - pub title: String, - pub slug: String, - pub description: Option, - pub is_active: Option, -} - -#[derive(InputObject)] -pub struct UpdatePageInputType { - pub title: Option, - pub slug: Option, - pub description: Option, - pub is_active: Option, -} - -#[derive(InputObject)] -pub struct CreateTextBlockInputType { - pub page_id: Uuid, - pub block_order: i32, - pub title: Option, - pub markdown: String, - pub is_active: Option, -} - -#[derive(InputObject)] -pub struct CreateChartBlockInputType { - pub page_id: Uuid, - pub block_order: i32, - pub title: String, - pub chart_type: String, - pub series: Vec, - pub config: Option, - pub is_active: Option, -} - -#[derive(InputObject)] -pub struct CreateDataPointInputType { - pub x: f64, - pub y: f64, - pub label: Option, - pub color: Option, -} - -#[derive(InputObject)] -pub struct CreateSettingsBlockInputType { - pub page_id: Uuid, - pub block_order: i32, - pub title: Option, - pub category: String, - pub editable: bool, - pub display_mode: String, - pub is_active: Option, -} - -#[derive(InputObject)] -pub struct PageFilterInputType { - pub title: Option, - pub slug: Option, - pub is_active: Option, - pub search: Option, -} - -// Enhanced Settings types for the settings center -#[derive(SimpleObject)] -pub struct SettingCenterType { - pub id: Uuid, - pub key: String, - pub value: Option, - pub value_type: String, - pub is_encrypted: Option, - pub is_editable: Option, - pub is_system: Option, - pub description: Option, - pub updated_at: chrono::DateTime, -} - -#[derive(InputObject)] -pub struct UpdateSettingCenterInput { - pub key: String, - pub value: Option, - pub description: Option, -} - -#[derive(InputObject)] -pub struct BatchUpdateSettingsInput { - pub updates: Vec, - pub reason: Option, -} - -/// 配置分类页面类型,包含页面信息和相关配置项 -#[derive(SimpleObject)] -pub struct CategoryPageType { - /// 页面信息,如果不存在则为 None - pub page: Option, - /// 该分类下的所有配置项 - pub settings: Vec, - /// 分类名称 - pub category: String, - /// 该分类下的配置项总数 - pub settings_count: i32, - /// 该分类下的系统配置项数量 - pub system_settings_count: i32, - /// 该分类下的可编辑配置项数量 - pub editable_settings_count: i32, -} - -/// 权限类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct PermissionType { - pub id: Uuid, - pub name: String, - pub description: Option, - pub resource: String, - pub action: String, - pub is_active: bool, - pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, -} - -/// 角色权限关联类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct RolePermissionType { - pub id: Uuid, - pub role_name: String, - pub permission: PermissionType, - pub granted_by: Option, - pub granted_at: chrono::DateTime, -} - -/// 用户角色关联类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct UserRoleType { - pub id: Uuid, - pub user_id: Uuid, - pub role_name: String, - pub granted_by: Option, - pub granted_at: chrono::DateTime, - pub expires_at: Option>, -} - -/// 权限策略类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct PolicyType { - pub role: String, - pub resource: String, - pub action: String, -} - -/// 权限检查结果类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct PermissionCheckResult { - pub user_id: String, - pub resource: String, - pub action: String, - pub has_permission: bool, - pub roles: Vec, -} - -/// 权限管理查询输入类型 -#[derive(Debug, InputObject)] -pub struct PermissionFilterInput { - pub resource: Option, - pub action: Option, - pub role_name: Option, - pub is_active: Option, -} - -/// 角色权限分配输入类型 -#[derive(Debug, InputObject)] -pub struct AssignRolePermissionInput { - pub role_name: String, - pub permission_id: Uuid, - pub granted_by: Uuid, -} - -/// 用户角色分配输入类型 -#[derive(Debug, InputObject)] -pub struct AssignUserRoleInput { - pub user_id: Uuid, - pub role_name: String, - pub granted_by: Uuid, - pub expires_at: Option>, -} - -#[derive(Debug, Clone, SimpleObject)] -pub struct PermissionPair { - pub resource: String, - pub action: String, -} - -// 站点与运营配置相关类型 - -/// 站点基本信息类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct SiteInfoType { - pub name: String, - pub locale_default: String, - pub locales_supported: Vec, -} - -/// 品牌配置类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct BrandConfigType { - pub logo_url: String, - pub primary_color: String, - pub dark_mode_default: bool, -} - -/// 页脚链接类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct FooterLinkType { - pub name: String, - pub url: String, - pub visible_to_guest: bool, -} - -/// 站点配置类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct SiteConfigType { - pub info: SiteInfoType, - pub brand: BrandConfigType, - pub footer_links: Vec, -} - -/// 横幅公告类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct BannerNoticeType { - pub enabled: bool, - pub text: std::collections::HashMap, // 多语言文本 -} - -/// 维护窗口类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct MaintenanceWindowType { - pub enabled: bool, - pub start_time: Option>, - pub end_time: Option>, - pub message: std::collections::HashMap, // 多语言消息 -} - -/// 弹窗公告类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct ModalAnnouncementType { - pub id: String, - pub title: std::collections::HashMap, // 多语言标题 - pub content: std::collections::HashMap, // 多语言内容 - pub start_time: chrono::DateTime, - pub end_time: chrono::DateTime, - pub audience: Vec, - pub priority: String, -} - -/// 公告维护配置类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct NoticeMaintenanceType { - pub banner: BannerNoticeType, - pub maintenance_window: MaintenanceWindowType, - pub modal_announcements: Vec, -} - -/// 文档链接类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct DocLinkType { - pub name: String, - pub url: String, - pub description: String, -} - -/// 聊天群组类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct ChatGroupType { - pub name: String, - pub url: Option, - pub qr_code: Option, - pub description: String, -} - -/// 支持渠道类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct SupportChannelsType { - pub email: String, - pub ticket_system: String, - pub chat_groups: Vec, - pub working_hours: std::collections::HashMap, // 多语言工作时间 -} - -/// 文档支持配置类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct DocsSupportType { - pub links: Vec, - pub channels: SupportChannelsType, -} - -/// 功能开关类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct FeatureSwitchesType { - pub registration_enabled: bool, - pub invite_code_required: bool, - pub email_verification: bool, -} - -/// 限制配置类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct LimitsConfigType { - pub max_users: i32, - pub max_invite_codes_per_user: i32, - pub session_timeout_hours: i32, -} - -/// 通知配置类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct NotificationConfigType { - pub welcome_email: bool, - pub system_announcements: bool, - pub maintenance_alerts: bool, -} - -/// 运营配置类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct OpsConfigType { - pub features: FeatureSwitchesType, - pub limits: LimitsConfigType, - pub notifications: NotificationConfigType, -} - -/// 完整的站点与运营配置类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct SiteOpsConfigType { - pub site: SiteConfigType, - pub notice_maintenance: NoticeMaintenanceType, - pub docs_support: DocsSupportType, - pub ops: OpsConfigType, -} - -/// 更新站点配置输入类型 -#[derive(Debug, InputObject)] -pub struct UpdateSiteConfigInput { - pub name: Option, - pub locale_default: Option, - pub locales_supported: Option>, - pub logo_url: Option, - pub primary_color: Option, - pub dark_mode_default: Option, - pub footer_links: Option>, -} - -/// 页脚链接输入类型 -#[derive(Debug, InputObject, Serialize, Deserialize)] -pub struct FooterLinkInput { - pub name: String, - pub url: String, - pub visible_to_guest: bool, -} - -/// 更新公告配置输入类型 -#[derive(Debug, InputObject)] -pub struct UpdateNoticeConfigInput { - pub banner_enabled: Option, - pub banner_text: Option>, - pub maintenance_enabled: Option, - pub maintenance_start_time: Option>, - pub maintenance_end_time: Option>, - pub maintenance_message: Option>, -} - -/// 更新弹窗公告输入类型 -#[derive(Debug, InputObject)] -pub struct UpdateModalAnnouncementInput { - pub id: String, - pub title: Option>, - pub content: Option>, - pub start_time: Option>, - pub end_time: Option>, - pub audience: Option>, - pub priority: Option, -} - -/// 更新文档支持配置输入类型 -#[derive(Debug, InputObject)] -pub struct UpdateDocsSupportInput { - pub links: Option>, - pub email: Option, - pub ticket_system: Option, - pub working_hours: Option>, -} - -/// 文档链接输入类型 -#[derive(Debug, InputObject, Serialize, Deserialize)] -pub struct DocLinkInput { - pub name: String, - pub url: String, - pub description: String, -} - -/// 更新运营配置输入类型 -#[derive(Debug, InputObject)] -pub struct UpdateOpsConfigInput { - pub registration_enabled: Option, - pub invite_code_required: Option, - pub email_verification: Option, - pub max_users: Option, - pub max_invite_codes_per_user: Option, - pub session_timeout_hours: Option, - pub welcome_email: Option, - pub system_announcements: Option, - pub maintenance_alerts: Option, -} - -/// 配置更新结果类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct ConfigUpdateResultType { - pub success: bool, - pub message: String, - pub updated_settings: Vec, -} - -/// 配置验证结果类型 -#[derive(Debug, Clone, SimpleObject)] -pub struct ConfigValidationResultType { - pub valid: bool, - pub errors: Vec, - pub warnings: Vec, -} diff --git a/src/graphql/types/blog.rs b/src/graphql/types/blog.rs new file mode 100644 index 0000000..7a0bf7f --- /dev/null +++ b/src/graphql/types/blog.rs @@ -0,0 +1,316 @@ +pub use async_graphql::{InputObject, SimpleObject}; +pub use chrono::{DateTime, Utc}; +pub use serde::{Deserialize, Serialize}; + +pub use crate::models::blog::{ + Blog as ModelBlog, BlogCategory as ModelBlogCategory, BlogTag as ModelBlogTag, +}; + +pub use input::*; +pub use output::*; + +pub mod output { + use super::*; + use crate::from_model; + use uuid::Uuid; + + #[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] + pub struct BlogCategory { + pub id: Uuid, + pub name: String, + pub slug: String, + pub description: Option, + pub color: Option, + pub icon: Option, + pub is_active: bool, + pub sort_order: i32, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: Option, + pub updated_by: Option, + } + + /// 博客标签 GraphQL 类型 + #[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] + pub struct BlogTag { + pub id: Uuid, + pub name: String, + pub slug: String, + pub description: Option, + pub color: Option, + pub is_active: bool, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: Option, + pub updated_by: Option, + } + + /// 博客文章 GraphQL 类型 + #[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] + pub struct Blog { + pub id: Uuid, + pub title: String, + pub slug: String, + pub excerpt: Option, + pub content: serde_json::Value, + pub category_id: Option, + pub status: String, + pub featured_image: Option, + pub meta_title: Option, + pub meta_description: Option, + pub published_at: Option>, + pub view_count: i32, + pub is_featured: bool, + pub is_active: bool, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: Option, + pub updated_by: Option, + } + + // 使用宏自动实现 From trait + from_model!( + BlogCategory, + ModelBlogCategory, + id, + name, + slug, + description, + color, + icon, + is_active, + sort_order, + created_at, + updated_at, + created_by, + updated_by + ); + + from_model!( + BlogTag, + ModelBlogTag, + id, + name, + slug, + description, + color, + is_active, + created_at, + updated_at, + created_by, + updated_by + ); + + from_model!( + Blog, + ModelBlog, + id, + title, + slug, + excerpt, + content, + category_id, + status, + featured_image, + meta_title, + meta_description, + published_at, + view_count, + is_featured, + is_active, + created_at, + updated_at, + created_by, + updated_by + ); + + /// 博客文章详情(包含分类和标签信息) + #[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] + pub struct BlogDetail { + #[graphql(flatten)] + pub blog: Blog, + pub category: Option, + pub tags: Vec, + } + + /// 博客标签关联 GraphQL 类型 + #[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] + pub struct BlogTagRelation { + pub id: Uuid, + pub blog_id: Uuid, + pub tag_id: Uuid, + pub created_at: DateTime, + } + + // 为 BlogTagRelation 实现 From trait + from_model!( + BlogTagRelation, + crate::models::blog::BlogTagRelation, + id, + blog_id, + tag_id, + created_at + ); + + // 为 BlogStats 实现 From trait + from_model!( + BlogStats, + crate::models::blog::BlogStats, + total_blogs, + published_blogs, + draft_blogs, + archived_blogs, + total_categories, + total_tags, + total_views + ); + + // 博客统计信息 + #[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] + pub struct BlogStats { + pub total_blogs: i64, + pub published_blogs: i64, + pub draft_blogs: i64, + pub archived_blogs: i64, + pub total_categories: i64, + pub total_tags: i64, + pub total_views: i64, + } + + // 注意:业务逻辑方法已经在 models/blog.rs 中实现 + // 这里的 GraphQL 类型主要用于 API 输出,业务逻辑应该在 model 层处理 + + impl BlogDetail { + /// 从博客文章创建详情对象 + pub fn new(blog: Blog, category: Option, tags: Vec) -> Self { + Self { + blog, + category, + tags, + } + } + } +} + +pub mod input { + use super::*; + use uuid::Uuid; + + // 创建博客分类输入 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct CreateBlogCategoryInput { + pub name: String, + pub slug: String, + pub description: Option, + pub color: Option, + pub icon: Option, + pub is_active: Option, + pub sort_order: Option, + } + + // 更新博客分类输入 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct UpdateBlogCategoryInput { + pub name: Option, + pub slug: Option, + pub description: Option, + pub color: Option, + pub icon: Option, + pub is_active: Option, + pub sort_order: Option, + } + + // 创建博客标签输入 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct CreateBlogTagInput { + pub name: String, + pub slug: String, + pub description: Option, + pub color: Option, + pub is_active: Option, + } + + // 更新博客标签输入 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct UpdateBlogTagInput { + pub name: Option, + pub slug: Option, + pub description: Option, + pub color: Option, + pub is_active: Option, + } + + // 创建博客文章输入 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct CreateBlogInput { + pub title: String, + pub slug: String, + pub excerpt: Option, + pub content: serde_json::Value, + pub category_id: Option, + pub status: Option, + pub featured_image: Option, + pub meta_title: Option, + pub meta_description: Option, + pub is_featured: Option, + pub is_active: Option, + pub tag_ids: Option>, + } + + // 更新博客文章输入 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct UpdateBlogInput { + pub title: Option, + pub slug: Option, + pub excerpt: Option, + pub content: Option, + pub category_id: Option, + pub status: Option, + pub featured_image: Option, + pub meta_title: Option, + pub meta_description: Option, + pub is_featured: Option, + pub is_active: Option, + pub tag_ids: Option>, + } + + // 博客过滤器 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct BlogFilterInput { + pub title: Option, + pub slug: Option, + pub category_id: Option, + pub status: Option, + pub is_featured: Option, + pub is_active: Option, + pub tag_ids: Option>, + pub search: Option, + pub date_from: Option>, + pub date_to: Option>, + } + + // 博客分类过滤器 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct BlogCategoryFilterInput { + pub name: Option, + pub slug: Option, + pub is_active: Option, + pub search: Option, + } + + // 博客标签过滤器 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct BlogTagFilterInput { + pub name: Option, + pub slug: Option, + pub is_active: Option, + pub search: Option, + } + + // 博客排序 + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct BlogSortInput { + pub field: String, + pub direction: String, // "asc" or "desc" + } +} diff --git a/src/graphql/types/config.rs b/src/graphql/types/config.rs new file mode 100644 index 0000000..b999b48 --- /dev/null +++ b/src/graphql/types/config.rs @@ -0,0 +1,169 @@ +use crate::graphql::guards::*; +use async_graphql::{InputObject, SimpleObject}; +use chrono::{DateTime, Utc}; +pub use input::*; +pub use output::*; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] +pub struct ConfigValue { + pub key: String, + pub value: serde_json::Value, + pub value_type: String, + pub description: Option, + pub category: String, +} + +pub mod output { + use serde::de::Error; + + use super::*; + #[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)] + pub struct Config { + pub id: Uuid, + pub key: String, + pub value: Option, + pub value_type: String, + pub description: Option, + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + pub category: Option, + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + pub is_encrypted: Option, + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + pub is_system: Option, + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + pub is_editable: Option, + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + pub created_at: DateTime, + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + pub updated_at: DateTime, + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + pub created_by: Option, + #[graphql(guard = "RequireWritePermission::new(\"config\")")] + pub updated_by: Option, + } + + impl Config { + /// 获取配置值的类型化版本 + pub fn get_typed_value(&self) -> Result + where + T: for<'de> Deserialize<'de>, + { + match &self.value { + Some(v) => serde_json::from_str(v), + None => Err(serde_json::Error::custom("No value set")), + } + } + + /// 设置配置值 + pub fn set_value(&mut self, value: &T) -> Result<(), serde_json::Error> + where + T: Serialize, + { + self.value = Some(serde_json::to_string(value)?); + Ok(()) + } + + /// 检查是否为特定类型 + pub fn is_type(&self, expected_type: &str) -> bool { + self.value_type == expected_type + } + + /// 获取布尔值 + pub fn get_bool(&self) -> Result { + if self.value_type == "boolean" { + self.value + .as_ref() + .and_then(|v| v.parse::().ok()) + .ok_or_else(|| "Invalid boolean value".to_string()) + } else { + Err("Setting is not a boolean type".to_string()) + } + } + + /// 获取数字值 + pub fn get_number(&self) -> Result { + if self.value_type == "number" { + self.value + .as_ref() + .and_then(|v| v.parse::().ok()) + .ok_or_else(|| "Invalid number value".to_string()) + } else { + Err("Setting is not a number type".to_string()) + } + } + + /// 获取JSON值 + pub fn get_json(&self) -> Result { + if self.value_type == "json" { + match &self.value { + Some(v) => serde_json::from_str(v), + None => Ok(serde_json::Value::Null), + } + } else { + Err(serde_json::Error::custom("Setting is not a JSON type")) + } + } + + /// 获取字符串值 + pub fn get_string(&self) -> Result { + if self.value_type == "string" { + Ok(self.value.clone().unwrap_or_default()) + } else { + Err("Setting is not a string type".to_string()) + } + } + } + + impl Default for Config { + fn default() -> Self { + Self { + id: Uuid::nil(), + key: String::new(), + value: None, + value_type: "string".to_string(), + description: None, + category: Some("general".to_string()), + is_encrypted: Some(false), + is_system: Some(false), + is_editable: Some(true), + created_at: Utc::now(), + updated_at: Utc::now(), + created_by: None, + updated_by: None, + } + } + } +} + +pub mod input { + use super::*; + #[derive(Debug, Clone, Serialize, Deserialize, InputObject)] + pub struct UpdateConfig { + pub key: String, + pub value: Option, + pub description: Option, + pub category: Option, + pub is_editable: Option, + } + #[derive(Default, Debug, Clone, Serialize, Deserialize)] + pub struct ConfigFilter { + pub category: Option, + pub is_system: Option, + pub is_editable: Option, + pub search: Option, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct ConfigHistory { + pub id: Uuid, + pub setting_id: Uuid, + pub old_value: Option, + pub new_value: Option, + pub changed_by: Option, + pub change_reason: Option, + pub created_at: DateTime, + } +} diff --git a/src/graphql/types/mod.rs b/src/graphql/types/mod.rs new file mode 100644 index 0000000..4632eae --- /dev/null +++ b/src/graphql/types/mod.rs @@ -0,0 +1,123 @@ +use async_graphql::{InputObject, SimpleObject}; +use serde::{Deserialize, Serialize}; + +pub mod blog; +pub mod config; +pub mod permission; +pub mod users; + +/// 用于自动实现 From for GraphQLType 的宏 +/// +/// 这个宏假设 GraphQL 类型和 Model 类型具有相同的字段名和类型。 +/// +/// # 用法 +/// ```rust +/// from_model!(GraphQLType, ModelType, field1, field2, field3, ...); +/// ``` +/// +/// # 示例 +/// ```rust +/// from_model!(BlogCategory, crate::models::blog::BlogCategory, +/// id, name, slug, description, color, icon, is_active, sort_order, +/// created_at, updated_at, created_by, updated_by +/// ); +/// ``` +#[macro_export] +macro_rules! from_model { + ($graphql_type:ident, $model_type:ty, $( $field:ident ),* $(,)?) => { + impl From<$model_type> for $graphql_type { + fn from(model: $model_type) -> Self { + Self { + $( + $field: model.$field, + )* + } + } + } + }; +} + +/// 更灵活的宏,支持字段映射和转换 +/// +/// # 用法 +/// ```rust +/// from_model_with_mapping!( +/// GraphQLType, +/// ModelType, +/// field1, +/// field2 => |val| val.map(|v| v.to_string()), +/// field3 => |val| val.into() +/// ); +/// ``` +#[macro_export] +macro_rules! from_model_with_mapping { + ( + $graphql_type:ident, + $model_type:ty, + $( $field:ident $( => $mapping:expr )? ),* $(,)? + ) => { + impl From<$model_type> for $graphql_type { + fn from(model: $model_type) -> Self { + Self { + $( + $field: from_model_with_mapping!(@apply_mapping model.$field, $( $mapping )?), + )* + } + } + } + }; + + // 没有映射,直接使用字段值 + (@apply_mapping $value:expr,) => { + $value + }; + + // 有映射函数,应用映射 + (@apply_mapping $value:expr, $mapping:expr) => { + ($mapping)($value) + }; +} + +pub use blog::*; +pub use config::*; +pub use config::*; +pub use permission::*; +pub use users::*; + +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct PaginationInput { + pub page: Option, + pub per_page: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] +pub struct PaginatedResult { + pub items: Vec, + pub total: i64, + pub page: i32, + pub per_page: i32, + pub total_pages: i32, +} + +impl PaginatedResult { + pub fn new(items: Vec, total: i64, page: i32, per_page: i32) -> Self { + let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32; + Self { + items, + total, + page, + per_page, + total_pages, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] +pub struct PageStats { + pub text_blocks: i32, + pub chart_blocks: i32, + pub settings_blocks: i32, + pub table_blocks: i32, + pub hero_blocks: i32, + pub total_blocks: i32, +} diff --git a/src/graphql/types/permission.rs b/src/graphql/types/permission.rs new file mode 100644 index 0000000..43c0558 --- /dev/null +++ b/src/graphql/types/permission.rs @@ -0,0 +1,102 @@ +use async_graphql::{InputObject, SimpleObject}; +use std::hash::{Hash, Hasher}; +use uuid::Uuid; + +/// 权限类型 +#[derive(Debug, Clone, SimpleObject)] +pub struct PermissionType { + pub id: Uuid, + pub name: String, + pub description: Option, + pub resource: String, + pub action: String, + pub is_active: bool, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +/// 角色权限关联类型 +#[derive(Debug, Clone, SimpleObject)] +pub struct RolePermissionType { + pub id: Uuid, + pub role_name: String, + pub permission: PermissionType, + pub granted_by: Option, + pub granted_at: chrono::DateTime, +} + +/// 用户角色关联类型 +#[derive(Debug, Clone, SimpleObject)] +pub struct UserRoleType { + pub id: Uuid, + pub user_id: Uuid, + pub role_name: String, + pub granted_by: Option, + pub granted_at: chrono::DateTime, + pub expires_at: Option>, +} + +/// 权限策略类型 +#[derive(Debug, Clone, SimpleObject)] +pub struct PolicyType { + pub role: String, + pub resource: String, + pub action: String, +} + +/// 权限检查结果类型 +#[derive(Debug, Clone, SimpleObject)] +pub struct PermissionCheckResult { + pub user_id: String, + pub resource: String, + pub action: String, + pub has_permission: bool, + pub roles: Vec, +} + +/// 权限管理查询输入类型 +#[derive(Debug, InputObject)] +pub struct PermissionFilterInput { + pub resource: Option, + pub action: Option, + pub role_name: Option, + pub is_active: Option, +} + +/// 角色权限分配输入类型 +#[derive(Debug, InputObject)] +pub struct AssignRolePermissionInput { + pub role_name: String, + pub permission_id: Uuid, + pub granted_by: Uuid, +} + +/// 用户角色分配输入类型 +#[derive(Debug, InputObject)] +pub struct AssignUserRoleInput { + pub user_id: Uuid, + pub role_name: String, + pub granted_by: Uuid, + pub expires_at: Option>, +} + +#[derive(Debug, Clone, SimpleObject)] +pub struct PermissionPair { + pub resource: String, + pub action: String, +} + +impl Hash for PermissionPair { + fn hash(&self, state: &mut H) { + self.resource.hash(state); + self.action.hash(state); + } +} + +impl PartialEq for PermissionPair { + fn eq(&self, other: &Self) -> bool { + self.resource == other.resource && self.action == other.action + } +} + +impl Eq for PermissionPair {} diff --git a/src/graphql/types/users.rs b/src/graphql/types/users.rs new file mode 100644 index 0000000..ed9efe4 --- /dev/null +++ b/src/graphql/types/users.rs @@ -0,0 +1,130 @@ +use crate::from_model; +use async_graphql::{InputObject, SimpleObject}; + +pub use input::*; +pub use output::*; + +pub mod input { + use super::*; + + #[derive(InputObject)] + pub struct RegisterInput { + pub username: String, + pub email: String, + pub password: String, + pub invite_code: String, + } + + #[derive(InputObject)] + pub struct CreateUserInput { + pub username: String, + pub email: String, + pub password: String, + } + + #[derive(InputObject)] + pub struct LoginInput { + pub username: String, + pub password: String, + } + + #[derive(InputObject)] + pub struct CreateInviteCodeInput { + pub expires_in_days: Option, + } + + #[derive(InputObject)] + pub struct ValidateInviteCodeInput { + pub code: String, + } + + #[derive(InputObject)] + pub struct InitializeAdminInput { + pub username: String, + pub email: String, + pub password: String, + } +} + +pub mod output { + use crate::from_model_with_mapping; + + use super::*; + use chrono::{DateTime, Utc}; + use uuid::Uuid; + + #[derive(SimpleObject)] + pub struct LoginResponse { + pub token: String, + pub user_id: String, + } + + #[derive(SimpleObject)] + pub struct InviteCodeResponse { + pub code: String, + pub expires_at: Option>, + } + + #[derive(SimpleObject)] + pub struct InitializeAdminResponse { + pub success: bool, + pub message: String, + pub user: Option, + } + + #[derive(SimpleObject)] + pub struct UserInfoRespnose { + pub total_users: i64, + pub total_active_users: i64, + pub total_inactive_users: i64, + pub total_admin_users: i64, + pub total_user_users: i64, + pub users: Vec, + } + + #[derive(Debug, Clone, SimpleObject)] + pub struct User { + pub id: Uuid, + pub username: String, + pub email: String, + pub is_activate: bool, + pub created_at: Option>, + pub updated_at: Option>, + } + + /// 包含组信息的用户 + #[derive(Debug, Clone, SimpleObject)] + pub struct UserWithGroups { + pub user: User, + pub groups: Vec, + } + + from_model!( + User, + crate::models::user::User, + id, + username, + email, + is_activate, + created_at, + updated_at + ); + + from_model_with_mapping!( + UserInfoRespnose, + crate::models::user::UserInfoRespnose, + total_users, + total_active_users, + total_inactive_users, + total_admin_users, + total_user_users, + users => |val: Vec| val.into_iter().map(|u| u.into()).collect(), + ); + + from_model_with_mapping!( + UserWithGroups, + crate::models::user::UserWithGroups, + user => |user: crate::models::user::User| user.into(), + groups , + ); +} diff --git a/src/main.rs b/src/main.rs index 0aa2ba3..ebdf8b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,9 +12,8 @@ use app::create_router; use clap::Parser; use cli::{ AddPolicyArgs, AssignRoleArgs, BlogArgs, BlogCommands, CheckPermissionArgs, Cli, Commands, - CreateBlogArgs, CreateCategoryArgs, CreateTagArgs, DeleteBlogArgs, ListBlogArgs, ListRolePermissionsArgs, ListUserRolesArgs, MigrateArgs, PermissionsArgs, PermissionsCommands, - RemovePolicyArgs, RemoveRoleArgs, ServeArgs, ShowBlogArgs, UpdateBlogArgs, + RemovePolicyArgs, RemoveRoleArgs, ServeArgs, }; use config::Config; use db::{create_pool, run_migrations}; @@ -23,6 +22,8 @@ use rustls; use std::process; use tokio::task; +use crate::graphql::types::PaginationInput; + #[tokio::main] async fn main() -> Result<(), Box> { let cli = Cli::parse(); @@ -512,8 +513,7 @@ fn print_config_info(config: &Config, args: &ServeArgs) { } async fn blog_command(args: BlogArgs) -> Result<(), Box> { - use models::blog::*; - use models::page_block::PaginationInput; + use crate::graphql::types::blog::*; use serde_json; use services::blog_service::BlogService; use uuid::Uuid; diff --git a/src/models/blog.rs b/src/models/blog.rs index 7a9abaf..8f4e011 100644 --- a/src/models/blog.rs +++ b/src/models/blog.rs @@ -1,11 +1,11 @@ -use async_graphql::{InputObject, SimpleObject}; +pub use crate::graphql::types::blog::input::*; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use uuid::Uuid; /// 博客分类模型 -#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)] +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct BlogCategory { pub id: Uuid, pub name: String, @@ -22,7 +22,7 @@ pub struct BlogCategory { } /// 博客标签模型 -#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)] +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct BlogTag { pub id: Uuid, pub name: String, @@ -37,7 +37,7 @@ pub struct BlogTag { } /// 博客文章模型 -#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)] +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Blog { pub id: Uuid, pub title: String, @@ -60,16 +60,15 @@ pub struct Blog { } /// 博客文章详情(包含分类和标签信息) -#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BlogDetail { - #[graphql(flatten)] pub blog: Blog, pub category: Option, pub tags: Vec, } /// 博客标签关联模型 -#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)] +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct BlogTagRelation { pub id: Uuid, pub blog_id: Uuid, @@ -77,126 +76,8 @@ pub struct BlogTagRelation { pub created_at: DateTime, } -// 创建博客分类输入 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct CreateBlogCategoryInput { - pub name: String, - pub slug: String, - pub description: Option, - pub color: Option, - pub icon: Option, - pub is_active: Option, - pub sort_order: Option, -} - -// 更新博客分类输入 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct UpdateBlogCategoryInput { - pub name: Option, - pub slug: Option, - pub description: Option, - pub color: Option, - pub icon: Option, - pub is_active: Option, - pub sort_order: Option, -} - -// 创建博客标签输入 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct CreateBlogTagInput { - pub name: String, - pub slug: String, - pub description: Option, - pub color: Option, - pub is_active: Option, -} - -// 更新博客标签输入 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct UpdateBlogTagInput { - pub name: Option, - pub slug: Option, - pub description: Option, - pub color: Option, - pub is_active: Option, -} - -// 创建博客文章输入 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct CreateBlogInput { - pub title: String, - pub slug: String, - pub excerpt: Option, - pub content: serde_json::Value, - pub category_id: Option, - pub status: Option, - pub featured_image: Option, - pub meta_title: Option, - pub meta_description: Option, - pub is_featured: Option, - pub is_active: Option, - pub tag_ids: Option>, -} - -// 更新博客文章输入 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct UpdateBlogInput { - pub title: Option, - pub slug: Option, - pub excerpt: Option, - pub content: Option, - pub category_id: Option, - pub status: Option, - pub featured_image: Option, - pub meta_title: Option, - pub meta_description: Option, - pub is_featured: Option, - pub is_active: Option, - pub tag_ids: Option>, -} - -// 博客过滤器 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct BlogFilterInput { - pub title: Option, - pub slug: Option, - pub category_id: Option, - pub status: Option, - pub is_featured: Option, - pub is_active: Option, - pub tag_ids: Option>, - pub search: Option, - pub date_from: Option>, - pub date_to: Option>, -} - -// 博客分类过滤器 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct BlogCategoryFilterInput { - pub name: Option, - pub slug: Option, - pub is_active: Option, - pub search: Option, -} - -// 博客标签过滤器 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct BlogTagFilterInput { - pub name: Option, - pub slug: Option, - pub is_active: Option, - pub search: Option, -} - -// 博客排序 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct BlogSortInput { - pub field: String, - pub direction: String, // "asc" or "desc" -} - // 博客统计信息 -#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct BlogStats { pub total_blogs: i64, pub published_blogs: i64, diff --git a/src/models/config.rs b/src/models/config.rs new file mode 100644 index 0000000..614d76c --- /dev/null +++ b/src/models/config.rs @@ -0,0 +1 @@ +pub use crate::graphql::types::config::*; diff --git a/src/models/mod.rs b/src/models/mod.rs index 49425d9..344be97 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,13 +1,34 @@ pub mod blog; +pub mod config; pub mod invite_code; pub mod kafka_message; pub mod page_block; -pub mod settings; pub mod user; pub use blog::*; +pub use config::*; pub use invite_code::*; pub use kafka_message::*; pub use page_block::*; -pub use settings::*; pub use user::*; + +pub struct PaginatedResult { + pub items: Vec, + pub total: i64, + pub page: i32, + pub per_page: i32, + pub total_pages: i32, +} + +impl PaginatedResult { + pub fn new(items: Vec, total: i64, page: i32, per_page: i32) -> Self { + let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32; + Self { + items, + total, + page, + per_page, + total_pages, + } + } +} diff --git a/src/models/page_block.rs b/src/models/page_block.rs index 268d1b3..5fac98c 100644 --- a/src/models/page_block.rs +++ b/src/models/page_block.rs @@ -578,44 +578,3 @@ pub struct BlockSortInput { pub field: String, pub direction: String, // "asc" or "desc" } - -// 分页输入 -#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] -pub struct PaginationInput { - pub page: Option, - pub per_page: Option, -} - -// 分页结果 -#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] -pub struct PaginatedResult { - pub items: Vec, - pub total: i64, - pub page: i32, - pub per_page: i32, - pub total_pages: i32, -} - -impl PaginatedResult { - pub fn new(items: Vec, total: i64, page: i32, per_page: i32) -> Self { - let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32; - Self { - items, - total, - page, - per_page, - total_pages, - } - } -} - -/// 页面统计信息 -#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] -pub struct PageStats { - pub text_blocks: i32, - pub chart_blocks: i32, - pub settings_blocks: i32, - pub table_blocks: i32, - pub hero_blocks: i32, - pub total_blocks: i32, -} diff --git a/src/models/settings.rs b/src/models/settings.rs deleted file mode 100644 index 6750ee0..0000000 --- a/src/models/settings.rs +++ /dev/null @@ -1,161 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{de::Error, Deserialize, Serialize}; -use sqlx::FromRow; -use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] -pub struct Setting { - pub id: Uuid, - pub key: String, - pub value: Option, - pub value_type: String, - pub description: Option, - pub category: Option, - pub is_encrypted: Option, - pub is_system: Option, - pub is_editable: Option, - pub created_at: DateTime, - pub updated_at: DateTime, - pub created_by: Option, - pub updated_by: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CreateSetting { - pub key: String, - pub value: Option, - pub value_type: String, - pub description: Option, - pub category: String, - pub is_encrypted: Option, - pub is_system: Option, - pub is_editable: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateSetting { - pub value: Option, - pub description: Option, - pub category: Option, - pub is_editable: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SettingValue { - pub key: String, - pub value: serde_json::Value, - pub value_type: String, - pub description: Option, - pub category: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SettingFilter { - pub category: Option, - pub is_system: Option, - pub is_editable: Option, - pub search: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SettingHistory { - pub id: Uuid, - pub setting_id: Uuid, - pub old_value: Option, - pub new_value: Option, - pub changed_by: Option, - pub change_reason: Option, - pub created_at: DateTime, -} - -impl Setting { - /// 获取配置值的类型化版本 - pub fn get_typed_value(&self) -> Result - where - T: for<'de> Deserialize<'de>, - { - match &self.value { - Some(v) => serde_json::from_str(v), - None => Err(serde_json::Error::custom("No value set")), - } - } - - /// 设置配置值 - pub fn set_value(&mut self, value: &T) -> Result<(), serde_json::Error> - where - T: Serialize, - { - self.value = Some(serde_json::to_string(value)?); - Ok(()) - } - - /// 检查是否为特定类型 - pub fn is_type(&self, expected_type: &str) -> bool { - self.value_type == expected_type - } - - /// 获取布尔值 - pub fn get_bool(&self) -> Result { - if self.value_type == "boolean" { - self.value - .as_ref() - .and_then(|v| v.parse::().ok()) - .ok_or_else(|| "Invalid boolean value".to_string()) - } else { - Err("Setting is not a boolean type".to_string()) - } - } - - /// 获取数字值 - pub fn get_number(&self) -> Result { - if self.value_type == "number" { - self.value - .as_ref() - .and_then(|v| v.parse::().ok()) - .ok_or_else(|| "Invalid number value".to_string()) - } else { - Err("Setting is not a number type".to_string()) - } - } - - /// 获取JSON值 - pub fn get_json(&self) -> Result { - if self.value_type == "json" { - match &self.value { - Some(v) => serde_json::from_str(v), - None => Ok(serde_json::Value::Null), - } - } else { - Err(serde_json::Error::custom("Setting is not a JSON type")) - } - } - - /// 获取字符串值 - pub fn get_string(&self) -> Result { - if self.value_type == "string" { - Ok(self.value.clone().unwrap_or_default()) - } else { - Err("Setting is not a string type".to_string()) - } - } -} - -impl Default for Setting { - fn default() -> Self { - Self { - id: Uuid::nil(), - key: String::new(), - value: None, - value_type: "string".to_string(), - description: None, - category: Some("general".to_string()), - is_encrypted: Some(false), - is_system: Some(false), - is_editable: Some(true), - created_at: Utc::now(), - updated_at: Utc::now(), - created_by: None, - updated_by: None, - } - } -} diff --git a/src/models/user.rs b/src/models/user.rs index c28e211..0f775d4 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,3 +1,4 @@ +pub use crate::graphql::types::users::input::*; use async_graphql::{Enum, SimpleObject}; use chrono::{DateTime, Utc}; use sea_query::Iden; @@ -18,13 +19,12 @@ impl Default for Role { } } -#[derive(Debug, Clone, FromRow, SimpleObject)] +#[derive(Debug, Clone, FromRow)] pub struct User { pub id: Uuid, pub username: String, pub email: String, - #[graphql(skip)] - pub password_hash: String, + pub password_hash: Option, pub role: Role, pub invite_code_id: Option, pub is_activate: bool, @@ -32,27 +32,14 @@ pub struct User { pub updated_at: Option>, } -#[derive(Debug, Serialize, Deserialize)] -pub struct CreateUserInput { - pub username: String, - pub email: String, - pub password: String, - pub invite_code: String, - pub role: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LoginInput { - pub username: String, - pub password: String, -} - #[derive(Iden, PartialEq, Eq)] pub enum Users { Table, Id, Username, Email, + PasswordHash, + InviteCodeId, Role, IsActivate, CreatedAt, @@ -68,6 +55,7 @@ impl TryFrom for Users { "username" => Ok(Users::Username), "email" => Ok(Users::Email), "role" => Ok(Users::Role), + "invite_code_id" => Ok(Users::InviteCodeId), "is_activate" => Ok(Users::IsActivate), "created_at" => Ok(Users::CreatedAt), "updated_at" => Ok(Users::UpdatedAt), @@ -76,13 +64,19 @@ impl TryFrom for Users { } } -#[derive(sqlx::FromRow, Debug, SimpleObject)] -pub struct UserInfoRow { - pub id: Uuid, - pub username: String, - pub email: String, - pub role: Role, - pub is_activate: bool, - pub created_at: DateTime, - pub updated_at: DateTime, +#[derive(Debug, Clone)] +pub struct UserInfoRespnose { + pub total_users: i64, + pub total_active_users: i64, + pub total_inactive_users: i64, + pub total_admin_users: i64, + pub total_user_users: i64, + pub users: Vec, +} + +/// 包含组信息的用户 +#[derive(Debug, Clone)] +pub struct UserWithGroups { + pub user: User, + pub groups: Vec, } diff --git a/src/services/blog_service.rs b/src/services/blog_service.rs index 6634bf6..a7f21fe 100644 --- a/src/services/blog_service.rs +++ b/src/services/blog_service.rs @@ -1,3 +1,6 @@ +use crate::graphql::types::PaginationInput; +use crate::models::{blog::*, PaginatedResult}; +use crate::services::query_builder::DynamicQueryBuilder; use anyhow::{anyhow, Result}; use chrono::Utc; use sea_query::{extension::postgres::PgExpr, Expr, Iden, Order, PostgresQueryBuilder, Query}; @@ -5,10 +8,6 @@ use sea_query_binder::SqlxBinder; use sqlx::{FromRow, PgPool}; use uuid::Uuid; -use crate::models::blog::*; -use crate::models::page_block::{PaginatedResult, PaginationInput}; -use crate::services::query_builder::DynamicQueryBuilder; - /// 博客相关表的列枚举 #[derive(Debug, Clone, Copy)] pub enum BlogsIden { diff --git a/src/services/casbin_service.rs b/src/services/casbin_service.rs index 226b18c..ab89856 100644 --- a/src/services/casbin_service.rs +++ b/src/services/casbin_service.rs @@ -198,6 +198,27 @@ impl CasbinService { .collect()) } + pub async fn get_user_permissions(&self, user_id: &str) -> Result> { + let enforcer = self.enforcer.read().await; + + let roles = enforcer.get_roles_for_user(user_id, None); + + use std::collections::HashSet; + let mut all_permissions = HashSet::new(); + + for role in roles { + let policies = enforcer.get_filtered_policy(0, vec![role.to_string()]); + + for policy in policies { + if policy.len() >= 3 { + all_permissions.insert((policy[1].to_string(), policy[2].to_string())); + } + } + } + + Ok(all_permissions.into_iter().collect()) + } + /// 重新加载策略 pub async fn reload_policy(&self) -> Result<()> { let mut enforcer = self.enforcer.write().await; diff --git a/src/services/settings_manager.rs b/src/services/config_manager.rs similarity index 62% rename from src/services/settings_manager.rs rename to src/services/config_manager.rs index 87700e6..5b65d74 100644 --- a/src/services/settings_manager.rs +++ b/src/services/config_manager.rs @@ -1,32 +1,89 @@ -use crate::models::Setting; -use crate::services::settings_service::SettingsService; +use crate::models::config::Config; +use crate::services::config_service::ConfigsService; use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; -/// 配置管理器,提供类型安全的配置访问和缓存 -pub struct SettingsManager { - settings_service: Arc, - cache: Arc>>, +pub mod keys { + // 应用配置 + pub const SITE_NAME: &str = "site.name"; + pub const SITE_DESCRIPTION: &str = "site.description"; + pub const SITE_KEYWORDS: &str = "site.keywords"; + pub const SITE_URL: &str = "site.url"; + pub const SITE_LOGO: &str = "site.logo"; + pub const SITE_COPYRIGHT: &str = "site.copyright"; + pub const SITE_ICP: &str = "site.icp"; + pub const SITE_ICP_URL: &str = "site.icp_url"; + pub const SITE_COLOR_STYLE: &str = "site.color_style"; + + // User + pub const USER_DEFAULT_AVATAR: &str = "user.default_avatar"; + pub const USER_DEFAULT_ROLE: &str = "user.default_role"; + pub const USER_NEED_REGISTER_INVITE_CODE: &str = "user.register_invite_code"; + pub const USER_NEED_REGISTER_EMAIL_VERIFICATION: &str = "user.register_email_verification"; + pub const OPEN_LOGIN: &str = "user.open_login"; + pub const OPEN_RESET_PASSWORD: &str = "user.open_reset_password"; + + // EMAIL + pub const EMAIL_SMTP_HOST: &str = "email.smtp_host"; + pub const EMAIL_SMTP_PORT: &str = "email.smtp_port"; + pub const EMAIL_SMTP_USER: &str = "email.smtp_user"; + pub const EMAIL_SMTP_PASSWORD: &str = "email.smtp_password"; + pub const EMAIL_SMTP_FROM: &str = "email.smtp_from"; + pub const EMAIL_SMTP_FROM_NAME: &str = "email.smtp_from_name"; + pub const EMAIL_SMTP_FROM_EMAIL: &str = "email.smtp_from_email"; + pub const EMAIL_SYSTEM_TEMPLATE: &str = "email.system_template"; + + // Blog + pub const BLOG_DEFAULT_AUTHOR: &str = "blog.default_author"; + pub const BLOG_DEFAULT_CATEGORY: &str = "blog.default_category"; + pub const BLOG_DEFAULT_TAG: &str = "blog.default_tag"; + pub const BLOG_OPEN_COMMENT: &str = "blog.open_comment"; + + // 日志配置 + pub const LOGGING_LEVEL: &str = "logging.level"; + pub const LOGGING_MAX_FILES: &str = "logging.max_files"; + pub const LOGGING_MAX_FILE_SIZE: &str = "logging.max_file_size"; + + // 缓存配置 + pub const CACHE_TTL: &str = "cache.ttl"; + pub const CACHE_MAX_SIZE: &str = "cache.max_size"; + + // Switches + pub const SWITCH_OPEN_REGISTER: &str = "switch.open_register"; + pub const SWITCH_OPEN_LOGIN: &str = "switch.open_login"; + pub const SWITCH_OPEN_RESET_PASSWORD: &str = "switch.open_reset_password"; + pub const SWITCH_OPEN_COMMENT: &str = "switch.open_comment"; + pub const SWITCH_OPEN_LIKE: &str = "switch.open_like"; + pub const SWITCH_OPEN_SHARE: &str = "switch.open_share"; + pub const SWITCH_OPEN_VIEW: &str = "switch.open_view"; +} + +pub struct ConfigsManager { + configs_service: Arc, + cache: Arc>>, cache_ttl: std::time::Duration, last_cache_update: Arc>, } -impl SettingsManager { - pub fn new(settings_service: SettingsService) -> Self { - Self { - settings_service: Arc::new(settings_service), +impl ConfigsManager { + pub async fn new(configs_service: ConfigsService) -> Self { + let manager = Self { + configs_service: Arc::new(configs_service), cache: Arc::new(RwLock::new(HashMap::new())), cache_ttl: std::time::Duration::from_secs(300), // 5分钟缓存 last_cache_update: Arc::new(RwLock::new(std::time::Instant::now())), - } + }; + + manager.refresh_cache().await.unwrap(); + manager } /// 刷新缓存 async fn refresh_cache(&self) -> Result<()> { - let settings = self.settings_service.get_all_settings().await?; + let settings = self.configs_service.get_all_configs().await?; let mut cache = self.cache.write().await; cache.clear(); @@ -47,7 +104,7 @@ impl SettingsManager { } /// 获取配置值(带缓存) - async fn get_cached_setting(&self, key: &str) -> Result> { + async fn get_cached_setting(&self, key: &str) -> Result> { if self.should_refresh_cache().await { self.refresh_cache().await?; } @@ -144,14 +201,15 @@ impl SettingsManager { T: Serialize, { // 更新数据库 - if let Some(setting) = self.settings_service.get_setting_by_key(key).await? { - let update_setting = crate::models::UpdateSetting { + if let Some(setting) = self.configs_service.get_config_by_key(key).await? { + let update_setting = crate::models::UpdateConfig { + key: key.to_string(), value: Some(serde_json::to_string(value)?), description: None, category: None, is_editable: None, }; - self.settings_service + self.configs_service .update_setting(setting.id, update_setting, uuid::Uuid::nil()) .await?; } @@ -165,7 +223,7 @@ impl SettingsManager { /// 批量设置配置值 pub async fn set_values(&self, updates: HashMap) -> Result<()> { let updates: Vec<(String, serde_json::Value)> = updates.into_iter().collect(); - self.settings_service + self.configs_service .batch_update_settings(updates, uuid::Uuid::nil()) .await?; @@ -181,7 +239,7 @@ impl SettingsManager { } /// 获取所有配置 - pub async fn get_all_settings(&self) -> Result> { + pub async fn get_all_settings(&self) -> Result> { if self.should_refresh_cache().await { self.refresh_cache().await?; } @@ -191,7 +249,7 @@ impl SettingsManager { } /// 获取分类配置 - pub async fn get_settings_by_category(&self, category: &str) -> Result> { + pub async fn get_settings_by_category(&self, category: &str) -> Result> { let all_settings = self.get_all_settings().await?; Ok(all_settings .into_iter() @@ -205,106 +263,7 @@ impl SettingsManager { } /// 获取配置元数据 - pub async fn get_setting_metadata(&self, key: &str) -> Result> { + pub async fn get_setting_metadata(&self, key: &str) -> Result> { self.get_cached_setting(key).await } } - -/// 预定义的配置键常量 -pub mod keys { - // 应用配置 - pub const APP_NAME: &str = "app.name"; - pub const APP_VERSION: &str = "app.version"; - pub const APP_DEBUG: &str = "app.debug"; - pub const APP_TIMEZONE: &str = "app.timezone"; - - // 数据库配置 - pub const DB_MAX_CONNECTIONS: &str = "database.max_connections"; - pub const DB_CONNECTION_TIMEOUT: &str = "database.connection_timeout"; - - // Kafka配置 - pub const KAFKA_MAX_RETRIES: &str = "kafka.max_retries"; - pub const KAFKA_RETRY_DELAY: &str = "kafka.retry_delay"; - - // 安全配置 - pub const SECURITY_SESSION_TIMEOUT: &str = "security.session_timeout"; - pub const SECURITY_MAX_LOGIN_ATTEMPTS: &str = "security.max_login_attempts"; - - // 日志配置 - pub const LOGGING_LEVEL: &str = "logging.level"; - pub const LOGGING_MAX_FILES: &str = "logging.max_files"; - - // 缓存配置 - pub const CACHE_TTL: &str = "cache.ttl"; - pub const CACHE_MAX_SIZE: &str = "cache.max_size"; -} - -/// 配置验证器 -pub struct SettingsValidator; - -impl SettingsValidator { - /// 验证必需的配置项 - pub async fn validate_required_settings(manager: &SettingsManager) -> Result> { - let required_keys = vec![ - keys::APP_NAME, - keys::APP_VERSION, - keys::DB_MAX_CONNECTIONS, - keys::KAFKA_MAX_RETRIES, - ]; - - let mut missing = Vec::new(); - - for key in required_keys { - if !manager.has_setting(key).await? { - missing.push(key.to_string()); - } - } - - Ok(missing) - } - - /// 验证配置值类型 - pub async fn validate_setting_types(manager: &SettingsManager) -> Result> { - let mut errors = Vec::new(); - - // 验证数字类型配置 - let number_keys = vec![ - keys::DB_MAX_CONNECTIONS, - keys::DB_CONNECTION_TIMEOUT, - keys::KAFKA_MAX_RETRIES, - keys::KAFKA_RETRY_DELAY, - keys::SECURITY_SESSION_TIMEOUT, - keys::SECURITY_MAX_LOGIN_ATTEMPTS, - keys::LOGGING_MAX_FILES, - keys::CACHE_TTL, - keys::CACHE_MAX_SIZE, - ]; - - for key in number_keys { - if let Some(setting) = manager.get_setting_metadata(key).await? { - if setting.value_type != "number" { - errors.push(format!( - "{} should be number type, got {}", - key, setting.value_type - )); - } - } - } - - // 验证布尔类型配置 - let bool_keys = vec![keys::APP_DEBUG]; - - for key in bool_keys { - if let Some(setting) = manager.get_setting_metadata(key).await? { - if setting.value_type != "boolean" { - errors.push(format!( - "{} should be boolean type, got {}", - key, setting.value_type - )); - } - } - } - - Ok(errors) - } -} diff --git a/src/services/settings_service.rs b/src/services/config_service.rs similarity index 59% rename from src/services/settings_service.rs rename to src/services/config_service.rs index 1a13bf9..23b6774 100644 --- a/src/services/settings_service.rs +++ b/src/services/config_service.rs @@ -1,4 +1,4 @@ -use crate::models::{CreateSetting, Setting, SettingFilter, SettingHistory, UpdateSetting}; +use crate::models::config::*; use crate::services::query_builder::DynamicQueryBuilder; use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; @@ -11,7 +11,7 @@ use std::collections::HashMap; use uuid::Uuid; #[derive(Iden, Clone)] -enum Settings { +enum Configs { Table, Id, Key, @@ -29,10 +29,10 @@ enum Settings { } #[derive(Iden, Clone)] -enum SettingsHistory { +enum ConfigsHistory { Table, Id, - SettingId, + ConfigId, OldValue, NewValue, ChangedBy, @@ -40,12 +40,12 @@ enum SettingsHistory { CreatedAt, } -pub struct SettingsService { +pub struct ConfigsService { pool: PgPool, query_builder: DynamicQueryBuilder, } -impl SettingsService { +impl ConfigsService { pub fn new(pool: PgPool) -> Self { let query_builder = DynamicQueryBuilder::new(pool.clone()); Self { @@ -54,97 +54,13 @@ impl SettingsService { } } - /// 获取数据库连接池的引用(用于事务) pub fn get_pool(&self) -> &PgPool { &self.pool } - /// 创建新的配置项 - pub async fn create_setting( - &self, - create_setting: CreateSetting, - user_id: Uuid, - ) -> Result { - // 检查key是否已存在 - let existing = sqlx::query!("SELECT id FROM settings WHERE key = $1", create_setting.key) - .fetch_optional(&self.pool) - .await?; - - if existing.is_some() { - return Err(anyhow!( - "Setting with key '{}' already exists", - create_setting.key - )); - } - - let setting = sqlx::query_as!( - Setting, - r#" - INSERT INTO settings (key, value, value_type, description, category, is_encrypted, is_system, is_editable, created_by) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - RETURNING id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - "#, - create_setting.key, - create_setting.value, - create_setting.value_type, - create_setting.description, - create_setting.category, - create_setting.is_encrypted.unwrap_or(false), - create_setting.is_system.unwrap_or(false), - create_setting.is_editable.unwrap_or(true), - user_id - ) - .fetch_one(&self.pool) - .await?; - - Ok(setting) - } - - /// 根据key获取配置项 - pub async fn get_setting_by_key(&self, key: &str) -> Result> { - let setting = sqlx::query_as!( - Setting, - r#" - SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - FROM settings WHERE key = $1 - "#, - key - ) - .fetch_optional(&self.pool) - .await?; - - Ok(setting) - } - - /// 根据ID获取配置项 - pub async fn get_setting_by_id(&self, id: Uuid) -> Result> { - let setting = sqlx::query_as!( - Setting, - r#" - SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - FROM settings WHERE id = $1 - "#, - id - ) - .fetch_optional(&self.pool) - .await?; - - Ok(setting) - } - - /// 获取所有配置项 - pub async fn get_all_settings(&self) -> Result> { + pub async fn get_all_configs(&self) -> Result> { let settings = sqlx::query_as!( - Setting, + Config, r#" SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, created_at as "created_at: DateTime", @@ -159,53 +75,52 @@ impl SettingsService { Ok(settings) } - /// 根据过滤条件获取配置项 (使用 sea-query 构建动态查询) - pub async fn get_settings_with_filter(&self, filter: &SettingFilter) -> Result> { + pub async fn get_settings_with_filter(&self, filter: &ConfigFilter) -> Result> { let mut query = Query::select(); query .columns([ - Settings::Id, - Settings::Key, - Settings::Value, - Settings::ValueType, - Settings::Description, - Settings::Category, - Settings::IsEncrypted, - Settings::IsSystem, - Settings::IsEditable, - Settings::CreatedAt, - Settings::UpdatedAt, - Settings::CreatedBy, - Settings::UpdatedBy, + Configs::Id, + Configs::Key, + Configs::Value, + Configs::ValueType, + Configs::Description, + Configs::Category, + Configs::IsEncrypted, + Configs::IsSystem, + Configs::IsEditable, + Configs::CreatedAt, + Configs::UpdatedAt, + Configs::CreatedBy, + Configs::UpdatedBy, ]) - .from(Settings::Table); + .from(Configs::Table); // 动态添加过滤条件 if let Some(category) = &filter.category { - query.and_where(Expr::col(Settings::Category).eq(category)); + query.and_where(Expr::col(Configs::Category).eq(category)); } if let Some(is_system) = &filter.is_system { - query.and_where(Expr::col(Settings::IsSystem).eq(*is_system)); + query.and_where(Expr::col(Configs::IsSystem).eq(*is_system)); } if let Some(is_editable) = &filter.is_editable { - query.and_where(Expr::col(Settings::IsEditable).eq(*is_editable)); + query.and_where(Expr::col(Configs::IsEditable).eq(*is_editable)); } if let Some(search) = &filter.search { let search_pattern = format!("%{}%", search); query.and_where( - Expr::col(Settings::Key) + Expr::col(Configs::Key) .ilike(&search_pattern) - .or(Expr::col(Settings::Description).ilike(&search_pattern)), + .or(Expr::col(Configs::Description).ilike(&search_pattern)), ); } // 添加排序 query - .order_by(Settings::Category, sea_query::Order::Asc) - .order_by(Settings::Key, sea_query::Order::Asc); + .order_by(Configs::Category, sea_query::Order::Asc) + .order_by(Configs::Key, sea_query::Order::Asc); // 构建并执行查询 let (sql, values) = query.build_sqlx(PostgresQueryBuilder); @@ -213,7 +128,7 @@ impl SettingsService { let mut settings = Vec::new(); for row in rows { - let setting = Setting { + let setting = Config { id: row.get("id"), key: row.get("key"), value: row.get("value"), @@ -238,18 +153,18 @@ impl SettingsService { pub async fn update_setting( &self, id: Uuid, - update_setting: UpdateSetting, + update_setting: UpdateConfig, user_id: Uuid, - ) -> Result { - let setting = self.get_setting_by_id(id).await?; - let setting = setting.ok_or_else(|| anyhow!("Setting not found"))?; + ) -> Result { + let setting = self.get_config_by_id(id).await?; + let setting = setting.ok_or_else(|| anyhow!("Config not found"))?; if !setting.is_editable.unwrap_or(false) { - return Err(anyhow!("Setting is not editable")); + return Err(anyhow!("Config is not editable")); } let updated_setting = sqlx::query_as!( - Setting, + Config, r#" UPDATE settings SET value = COALESCE($1, value), @@ -277,39 +192,58 @@ impl SettingsService { Ok(updated_setting) } - /// 删除配置项 - pub async fn delete_setting(&self, id: Uuid) -> Result { - let setting = self.get_setting_by_id(id).await?; - let setting = setting.ok_or_else(|| anyhow!("Setting not found"))?; - - if setting.is_system.unwrap_or(false) { - return Err(anyhow!("Cannot delete system settings")); - } - - let result = sqlx::query!("DELETE FROM settings WHERE id = $1", id) - .execute(&self.pool) + pub async fn get_config_by_id(&self, id: Uuid) -> Result> { + let config = sqlx::query_as!( + Config, + r#" + SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime", + created_by, updated_by + FROM settings WHERE id = $1 + "#, + id + ) + .fetch_optional(&self.pool) .await?; - Ok(result.rows_affected() > 0) + Ok(config) + } + + pub async fn get_config_by_key(&self, key: &str) -> Result> { + let config = sqlx::query_as!( + Config, + r#" + SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable, + created_at as "created_at: DateTime", + updated_at as "updated_at: DateTime", + created_by, updated_by + FROM settings WHERE key = $1 + "#, + key + ) + .fetch_optional(&self.pool) + .await?; + + Ok(config) } - /// 批量更新配置项 pub async fn batch_update_settings( &self, updates: Vec<(String, Value)>, user_id: Uuid, - ) -> Result> { + ) -> Result> { let mut updated_settings = Vec::new(); for (key, value) in updates { - if let Some(setting) = self.get_setting_by_key(&key).await? { + if let Some(setting) = self.get_config_by_key(&key).await? { if !setting.is_editable.unwrap_or(false) { continue; // 跳过不可编辑的配置 } let value_str = serde_json::to_string(&value)?; let updated_setting = sqlx::query_as!( - Setting, + Config, r#" UPDATE settings SET value = $1, updated_by = $2, updated_at = CURRENT_TIMESTAMP @@ -334,9 +268,9 @@ impl SettingsService { } /// 获取配置历史 - pub async fn get_setting_history(&self, setting_id: Uuid) -> Result> { + pub async fn get_setting_history(&self, setting_id: Uuid) -> Result> { let history = sqlx::query_as!( - SettingHistory, + ConfigHistory, r#" SELECT id, setting_id, old_value, new_value, changed_by, change_reason, created_at as "created_at: DateTime" @@ -401,42 +335,42 @@ impl SettingsService { } /// 重置配置到默认值 - pub async fn reset_to_defaults(&self, user_id: Uuid) -> Result> { + pub async fn reset_to_defaults(&self, user_id: Uuid) -> Result> { // 这里可以实现重置逻辑,比如从配置文件重新加载默认值 // 暂时返回空列表 Ok(Vec::new()) } /// 导出配置 (使用 sea-query 构建动态查询) - pub async fn export_settings(&self, category: Option<&str>) -> Result> { + pub async fn export_settings(&self, category: Option<&str>) -> Result> { let mut query = Query::select(); query .columns([ - Settings::Id, - Settings::Key, - Settings::Value, - Settings::ValueType, - Settings::Description, - Settings::Category, - Settings::IsEncrypted, - Settings::IsSystem, - Settings::IsEditable, - Settings::CreatedAt, - Settings::UpdatedAt, - Settings::CreatedBy, - Settings::UpdatedBy, + Configs::Id, + Configs::Key, + Configs::Value, + Configs::ValueType, + Configs::Description, + Configs::Category, + Configs::IsEncrypted, + Configs::IsSystem, + Configs::IsEditable, + Configs::CreatedAt, + Configs::UpdatedAt, + Configs::CreatedBy, + Configs::UpdatedBy, ]) - .from(Settings::Table); + .from(Configs::Table); // 根据分类过滤 if let Some(cat) = category { - query.and_where(Expr::col(Settings::Category).eq(cat)); + query.and_where(Expr::col(Configs::Category).eq(cat)); } // 添加排序 query - .order_by(Settings::Category, sea_query::Order::Asc) - .order_by(Settings::Key, sea_query::Order::Asc); + .order_by(Configs::Category, sea_query::Order::Asc) + .order_by(Configs::Key, sea_query::Order::Asc); // 构建并执行查询 let (sql, values) = query.build_sqlx(PostgresQueryBuilder); @@ -444,7 +378,7 @@ impl SettingsService { let mut settings = Vec::new(); for row in rows { - let setting = Setting { + let setting = Config { id: row.get("id"), key: row.get("key"), value: row.get("value"), @@ -465,54 +399,54 @@ impl SettingsService { Ok(settings) } - /// 导入配置 - pub async fn import_settings( - &self, - settings: Vec, - user_id: Uuid, - ) -> Result> { - let mut imported_settings = Vec::new(); + // /// 导入配置 + // pub async fn import_settings( + // &self, + // settings: Vec, + // user_id: Uuid, + // ) -> Result> { + // let mut imported_settings = Vec::new(); - for create_setting in settings { - // 检查是否已存在 - if let Some(existing) = self.get_setting_by_key(&create_setting.key).await? { - // 如果存在且可编辑,则更新 - if existing.is_editable.unwrap_or(false) { - let update_setting = UpdateSetting { - value: create_setting.value, - description: create_setting.description, - category: Some(create_setting.category), - is_editable: Some(create_setting.is_editable.unwrap_or(true)), - }; - let updated = self - .update_setting(existing.id, update_setting, user_id) - .await?; - imported_settings.push(updated); - } - } else { - // 如果不存在,则创建 - let new_setting = self.create_setting(create_setting, user_id).await?; - imported_settings.push(new_setting); - } - } + // for create_setting in settings { + // // 检查是否已存在 + // if let Some(existing) = self.get_setting_by_key(&create_setting.key).await? { + // // 如果存在且可编辑,则更新 + // if existing.is_editable.unwrap_or(false) { + // let update_setting = UpdateConfig { + // value: create_setting.value, + // description: create_setting.description, + // category: Some(create_setting.category), + // is_editable: Some(create_setting.is_editable.unwrap_or(true)), + // }; + // let updated = self + // .update_setting(existing.id, update_setting, user_id) + // .await?; + // imported_settings.push(updated); + // } + // } else { + // // 如果不存在,则创建 + // let new_setting = self.create_setting(create_setting, user_id).await?; + // imported_settings.push(new_setting); + // } + // } - Ok(imported_settings) - } + // Ok(imported_settings) + // } /// 分页查询配置项 (sea-query 分页示例) pub async fn get_settings_paginated( &self, - filter: &SettingFilter, + filter: &ConfigFilter, page: u64, page_size: u64, - ) -> Result<(Vec, u64)> { + ) -> Result<(Vec, u64)> { let offset = (page - 1) * page_size; // 查询总数 let mut count_query = Query::select(); count_query - .expr(Expr::col(Settings::Id).count()) - .from(Settings::Table); + .expr(Expr::col(Configs::Id).count()) + .from(Configs::Table); // 应用过滤条件 self.apply_filter_conditions(&mut count_query, filter); @@ -526,27 +460,27 @@ impl SettingsService { let mut data_query = Query::select(); data_query .columns([ - Settings::Id, - Settings::Key, - Settings::Value, - Settings::ValueType, - Settings::Description, - Settings::Category, - Settings::IsEncrypted, - Settings::IsSystem, - Settings::IsEditable, - Settings::CreatedAt, - Settings::UpdatedAt, - Settings::CreatedBy, - Settings::UpdatedBy, + Configs::Id, + Configs::Key, + Configs::Value, + Configs::ValueType, + Configs::Description, + Configs::Category, + Configs::IsEncrypted, + Configs::IsSystem, + Configs::IsEditable, + Configs::CreatedAt, + Configs::UpdatedAt, + Configs::CreatedBy, + Configs::UpdatedBy, ]) - .from(Settings::Table); + .from(Configs::Table); self.apply_filter_conditions(&mut data_query, filter); data_query - .order_by(Settings::Category, sea_query::Order::Asc) - .order_by(Settings::Key, sea_query::Order::Asc) + .order_by(Configs::Category, sea_query::Order::Asc) + .order_by(Configs::Key, sea_query::Order::Asc) .limit(page_size) .offset(offset); @@ -555,7 +489,7 @@ impl SettingsService { let mut settings = Vec::new(); for row in rows { - let setting = Setting { + let setting = Config { id: row.get("id"), key: row.get("key"), value: row.get("value"), @@ -584,75 +518,75 @@ impl SettingsService { value_types: Option>, is_system: Option, date_range: Option<(chrono::DateTime, chrono::DateTime)>, - ) -> Result> { + ) -> Result> { let mut query = Query::select(); query .columns([ - Settings::Id, - Settings::Key, - Settings::Value, - Settings::ValueType, - Settings::Description, - Settings::Category, - Settings::IsEncrypted, - Settings::IsSystem, - Settings::IsEditable, - Settings::CreatedAt, - Settings::UpdatedAt, - Settings::CreatedBy, - Settings::UpdatedBy, + Configs::Id, + Configs::Key, + Configs::Value, + Configs::ValueType, + Configs::Description, + Configs::Category, + Configs::IsEncrypted, + Configs::IsSystem, + Configs::IsEditable, + Configs::CreatedAt, + Configs::UpdatedAt, + Configs::CreatedBy, + Configs::UpdatedBy, ]) - .from(Settings::Table); + .from(Configs::Table); // 搜索关键词 if let Some(term) = search_term { let search_pattern = format!("%{}%", term); query.and_where( - Expr::col(Settings::Key) + Expr::col(Configs::Key) .ilike(&search_pattern) - .or(Expr::col(Settings::Description).ilike(&search_pattern)) - .or(Expr::col(Settings::Value).ilike(&search_pattern)), + .or(Expr::col(Configs::Description).ilike(&search_pattern)) + .or(Expr::col(Configs::Value).ilike(&search_pattern)), ); } // 多个分类 if let Some(cats) = categories { if !cats.is_empty() { - query.and_where(Expr::col(Settings::Category).is_in(cats)); + query.and_where(Expr::col(Configs::Category).is_in(cats)); } } // 多个值类型 if let Some(types) = value_types { if !types.is_empty() { - query.and_where(Expr::col(Settings::ValueType).is_in(types)); + query.and_where(Expr::col(Configs::ValueType).is_in(types)); } } // 系统设置 if let Some(sys) = is_system { - query.and_where(Expr::col(Settings::IsSystem).eq(sys)); + query.and_where(Expr::col(Configs::IsSystem).eq(sys)); } // 日期范围 if let Some((start_date, end_date)) = date_range { query.and_where( - Expr::col(Settings::CreatedAt) + Expr::col(Configs::CreatedAt) .gte(start_date) - .and(Expr::col(Settings::CreatedAt).lte(end_date)), + .and(Expr::col(Configs::CreatedAt).lte(end_date)), ); } query - .order_by(Settings::Category, sea_query::Order::Asc) - .order_by(Settings::Key, sea_query::Order::Asc); + .order_by(Configs::Category, sea_query::Order::Asc) + .order_by(Configs::Key, sea_query::Order::Asc); let (sql, values) = query.build_sqlx(PostgresQueryBuilder); let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?; let mut settings = Vec::new(); for row in rows { - let setting = Setting { + let setting = Config { id: row.get("id"), key: row.get("key"), value: row.get("value"), @@ -676,37 +610,37 @@ impl SettingsService { /// 批量操作 (动态更新多个配置项) pub async fn batch_update_by_conditions( &self, - conditions: &SettingFilter, + conditions: &ConfigFilter, updates: HashMap, user_id: Uuid, ) -> Result { // 构建更新查询 let mut update_query = Query::update(); - update_query.table(Settings::Table); + update_query.table(Configs::Table); // 添加更新字段 for (field, value) in updates { match field.as_str() { "description" => { if let Some(desc) = value.as_str() { - update_query.value(Settings::Description, desc); + update_query.value(Configs::Description, desc); } } "category" => { if let Some(cat) = value.as_str() { - update_query.value(Settings::Category, cat); + update_query.value(Configs::Category, cat); } } "is_editable" => { if let Some(editable) = value.as_bool() { - update_query.value(Settings::IsEditable, editable); + update_query.value(Configs::IsEditable, editable); } } _ => {} // 忽略未知字段 } } - update_query.value(Settings::UpdatedBy, user_id); + update_query.value(Configs::UpdatedBy, user_id); // 应用过滤条件 self.apply_filter_conditions(&mut update_query, conditions); @@ -718,67 +652,67 @@ impl SettingsService { } /// 辅助方法:应用过滤条件到查询 - fn apply_filter_conditions(&self, query: &mut T, filter: &SettingFilter) + fn apply_filter_conditions(&self, query: &mut T, filter: &ConfigFilter) where T: sea_query::QueryStatementWriter + sea_query::ConditionalStatement, { if let Some(category) = &filter.category { - query.and_where(Expr::col(Settings::Category).eq(category)); + query.and_where(Expr::col(Configs::Category).eq(category)); } if let Some(is_system) = &filter.is_system { - query.and_where(Expr::col(Settings::IsSystem).eq(*is_system)); + query.and_where(Expr::col(Configs::IsSystem).eq(*is_system)); } if let Some(is_editable) = &filter.is_editable { - query.and_where(Expr::col(Settings::IsEditable).eq(*is_editable)); + query.and_where(Expr::col(Configs::IsEditable).eq(*is_editable)); } if let Some(search) = &filter.search { let search_pattern = format!("%{}%", search); query.and_where( - Expr::col(Settings::Key) + Expr::col(Configs::Key) .ilike(&search_pattern) - .or(Expr::col(Settings::Description).ilike(&search_pattern)), + .or(Expr::col(Configs::Description).ilike(&search_pattern)), ); } } /// 使用查询构建器的简化过滤查询示例 - pub async fn get_settings_with_builder(&self, filter: &SettingFilter) -> Result> { + pub async fn get_settings_with_builder(&self, filter: &ConfigFilter) -> Result> { let rows = self .query_builder - .select(Settings::Table) + .select(Configs::Table) .columns([ - Settings::Id, - Settings::Key, - Settings::Value, - Settings::ValueType, - Settings::Description, - Settings::Category, - Settings::IsEncrypted, - Settings::IsSystem, - Settings::IsEditable, - Settings::CreatedAt, - Settings::UpdatedAt, - Settings::CreatedBy, - Settings::UpdatedBy, + Configs::Id, + Configs::Key, + Configs::Value, + Configs::ValueType, + Configs::Description, + Configs::Category, + Configs::IsEncrypted, + Configs::IsSystem, + Configs::IsEditable, + Configs::CreatedAt, + Configs::UpdatedAt, + Configs::CreatedBy, + Configs::UpdatedBy, ]) - .condition_option(Settings::Category, filter.category.clone()) - .condition_option(Settings::IsSystem, filter.is_system) - .condition_option(Settings::IsEditable, filter.is_editable) + .condition_option(Configs::Category, filter.category.clone()) + .condition_option(Configs::IsSystem, filter.is_system) + .condition_option(Configs::IsEditable, filter.is_editable) .search_like( - vec![Settings::Key, Settings::Description], + vec![Configs::Key, Configs::Description], filter.search.as_deref(), ) - .order_by(Settings::Category, Order::Asc) - .order_by(Settings::Key, Order::Asc) + .order_by(Configs::Category, Order::Asc) + .order_by(Configs::Key, Order::Asc) .fetch_all() .await?; let mut settings = Vec::new(); for row in rows { - let setting = Setting { + let setting = Config { id: row.get("id"), key: row.get("key"), value: row.get("value"), @@ -802,19 +736,19 @@ impl SettingsService { /// 使用查询构建器的分页查询示例 pub async fn get_settings_paginated_with_builder( &self, - filter: &SettingFilter, + filter: &ConfigFilter, page: u64, page_size: u64, - ) -> Result<(Vec, i64)> { + ) -> Result<(Vec, i64)> { // 获取总数 let total_count = self .query_builder - .select(Settings::Table) - .condition_option(Settings::Category, filter.category.clone()) - .condition_option(Settings::IsSystem, filter.is_system) - .condition_option(Settings::IsEditable, filter.is_editable) + .select(Configs::Table) + .condition_option(Configs::Category, filter.category.clone()) + .condition_option(Configs::IsSystem, filter.is_system) + .condition_option(Configs::IsEditable, filter.is_editable) .search_like( - vec![Settings::Key, Settings::Description], + vec![Configs::Key, Configs::Description], filter.search.as_deref(), ) .count() @@ -823,38 +757,38 @@ impl SettingsService { // 获取分页数据 let rows = self .query_builder - .select(Settings::Table) + .select(Configs::Table) .columns([ - Settings::Id, - Settings::Key, - Settings::Value, - Settings::ValueType, - Settings::Description, - Settings::Category, - Settings::IsEncrypted, - Settings::IsSystem, - Settings::IsEditable, - Settings::CreatedAt, - Settings::UpdatedAt, - Settings::CreatedBy, - Settings::UpdatedBy, + Configs::Id, + Configs::Key, + Configs::Value, + Configs::ValueType, + Configs::Description, + Configs::Category, + Configs::IsEncrypted, + Configs::IsSystem, + Configs::IsEditable, + Configs::CreatedAt, + Configs::UpdatedAt, + Configs::CreatedBy, + Configs::UpdatedBy, ]) - .condition_option(Settings::Category, filter.category.clone()) - .condition_option(Settings::IsSystem, filter.is_system) - .condition_option(Settings::IsEditable, filter.is_editable) + .condition_option(Configs::Category, filter.category.clone()) + .condition_option(Configs::IsSystem, filter.is_system) + .condition_option(Configs::IsEditable, filter.is_editable) .search_like( - vec![Settings::Key, Settings::Description], + vec![Configs::Key, Configs::Description], filter.search.as_deref(), ) - .order_by(Settings::Category, Order::Asc) - .order_by(Settings::Key, Order::Asc) + .order_by(Configs::Category, Order::Asc) + .order_by(Configs::Key, Order::Asc) .paginate(page, page_size) .fetch_all() .await?; let mut settings = Vec::new(); for row in rows { - let setting = Setting { + let setting = Config { id: row.get("id"), key: row.get("key"), value: row.get("value"), diff --git a/src/services/mod.rs b/src/services/mod.rs index a55a537..0140c51 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,10 +1,9 @@ pub mod blog_service; pub mod casbin_service; +pub mod config_manager; +pub mod config_service; pub mod invite_code_service; pub mod mosaic_service; -pub mod page_block_service; pub mod query_builder; -pub mod settings_manager; -pub mod settings_service; pub mod system_config_service; pub mod user_service; diff --git a/src/services/page_block_service.rs b/src/services/page_block_service.rs deleted file mode 100644 index 3c08eee..0000000 --- a/src/services/page_block_service.rs +++ /dev/null @@ -1,1546 +0,0 @@ -use chrono::{DateTime, Utc}; -use sea_query::{Expr, PostgresQueryBuilder, Query}; -use sea_query_binder::SqlxBinder; -use sqlx::{PgPool, Row}; -use thiserror::Error; -use tracing::error; -use uuid::Uuid; - -use crate::models::page_block::*; - -#[derive(Debug, Error)] -pub enum PageBlockError { - #[error("数据库错误: {0}")] - DatabaseError(#[from] sqlx::Error), - #[error("未找到: {0}")] - NotFound(String), - #[error("验证错误: {0}")] - ValidationError(String), - #[error("权限错误: {0}")] - PermissionError(String), -} - -pub struct PageBlockService { - pool: PgPool, -} - -impl PageBlockService { - pub fn new(pool: PgPool) -> Self { - Self { pool } - } - - /// 创建新页面 - pub async fn create_page( - &self, - input: CreatePageInput, - user_id: Uuid, - ) -> Result { - let result = sqlx::query_as!( - Page, - r#" - INSERT INTO pages (title, slug, description, is_active, created_by) - VALUES ($1, $2, $3, $4, $5) - RETURNING id, title, slug, description, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - "#, - input.title, - input.slug, - input.description, - input.is_active.unwrap_or(true), - user_id - ) - .fetch_one(&self.pool) - .await?; - - Ok(Page { - id: result.id, - title: result.title, - slug: result.slug, - description: result.description, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - created_by: result.created_by, - updated_by: result.updated_by, - }) - } - - /// 根据ID获取页面 - pub async fn get_page_by_id(&self, page_id: Uuid) -> Result { - let result = sqlx::query_as!( - Page, - r#" - SELECT id, title, slug, description, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - FROM pages WHERE id = $1 AND is_active = true - "#, - page_id - ) - .fetch_optional(&self.pool) - .await?; - - match result { - Some(result) => Ok(Page { - id: result.id, - title: result.title, - slug: result.slug, - description: result.description, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - created_by: result.created_by, - updated_by: result.updated_by, - }), - None => Err(PageBlockError::NotFound(format!("页面 {} 未找到", page_id))), - } - } - - /// 根据slug获取页面 - pub async fn get_page_by_slug(&self, slug: &str) -> Result { - let result = sqlx::query_as!( - Page, - r#" - SELECT id, title, slug, description, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - FROM pages WHERE slug = $1 AND is_active = true - "#, - slug - ) - .fetch_optional(&self.pool) - .await?; - - match result { - Some(result) => Ok(Page { - id: result.id, - title: result.title, - slug: result.slug, - description: result.description, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - created_by: result.created_by, - updated_by: result.updated_by, - }), - None => Err(PageBlockError::NotFound(format!( - "页面 slug {} 未找到", - slug - ))), - } - } - - /// 获取页面列表(使用 sea-query 构建动态查询) - pub async fn get_pages( - &self, - filter: Option, - limit: Option, - offset: Option, - ) -> Result, PageBlockError> { - // 使用 sea-query 构建查询 - let mut query = Query::select(); - use Pages::Pages; - query - .columns([ - (Pages::Table, Pages::Id), - (Pages::Table, Pages::Title), - (Pages::Table, Pages::Slug), - (Pages::Table, Pages::Description), - (Pages::Table, Pages::IsActive), - (Pages::Table, Pages::CreatedAt), - (Pages::Table, Pages::UpdatedAt), - (Pages::Table, Pages::CreatedBy), - (Pages::Table, Pages::UpdatedBy), - ]) - .from(Pages::Table) - .and_where(Expr::col((Pages::Table, Pages::IsActive)).eq(true)); - - // 添加过滤条件 - if let Some(filter) = filter { - if let Some(title) = filter.title { - query.and_where( - Expr::col((Pages::Table, Pages::Title)).like(format!("%{}%", title)), - ); - } - if let Some(search) = filter.search { - query.and_where( - Expr::col((Pages::Table, Pages::Title)) - .like(format!("%{}%", search)) - .or(Expr::col((Pages::Table, Pages::Description)) - .like(format!("%{}%", search))), - ); - } - } - - // 添加排序 - query.order_by((Pages::Table, Pages::CreatedAt), sea_query::Order::Desc); - - // 添加分页 - if let Some(limit) = limit { - query.limit(limit as u64); - } - if let Some(offset) = offset { - query.offset(offset as u64); - } - - // 构建并执行查询 - let (sql, values) = query.build_sqlx(PostgresQueryBuilder); - let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?; - - let pages = rows - .into_iter() - .map(|row| Page { - id: row.get("id"), - title: row.get("title"), - slug: row.get("slug"), - description: row.get("description"), - is_active: row.get("is_active"), - created_at: chrono::DateTime::from( - row.get::, _>("created_at"), - ), - updated_at: chrono::DateTime::from( - row.get::, _>("updated_at"), - ), - created_by: row.get("created_by"), - updated_by: row.get("updated_by"), - }) - .collect(); - - Ok(pages) - } - - /// 获取页面的所有块 - pub async fn get_page_blocks(&self, page_id: Uuid) -> Result, PageBlockError> { - let mut blocks = Vec::new(); - - // 获取文本块 - let text_blocks = sqlx::query!( - r#" - SELECT id, page_id, block_order, title, markdown, is_active, created_at as "created_at: DateTime", updated_at as "updated_at: DateTime" - FROM text_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order - "#, - page_id - ) - .fetch_all(&self.pool) - .await?; - - for row in text_blocks { - blocks.push(Block::TextBlock(TextBlock { - id: row.id, - page_id: row.page_id, - block_order: row.block_order, - title: row.title, - markdown: row.markdown, - is_active: row.is_active, - created_at: chrono::DateTime::from(row.created_at), - updated_at: chrono::DateTime::from(row.updated_at), - })); - } - - // 获取图表块 - let chart_blocks = sqlx::query!( - r#" - SELECT id, page_id, block_order, title, chart_type, config, is_active, created_at as "created_at: DateTime", updated_at as "updated_at: DateTime" - FROM chart_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order - "#, - page_id - ) - .fetch_all(&self.pool) - .await?; - - for row in chart_blocks { - // 获取数据点 - let data_points = sqlx::query!( - r#" - SELECT id, chart_block_id, x, y, label, color - FROM data_points WHERE chart_block_id = $1 ORDER BY x - "#, - row.id - ) - .fetch_all(&self.pool) - .await?; - - let series = data_points - .into_iter() - .map(|dp| DataPoint { - id: dp.id, - chart_block_id: dp.chart_block_id, - x: dp.x, - y: dp.y, - label: dp.label, - color: dp.color, - }) - .collect(); - - blocks.push(Block::ChartBlock(ChartBlock { - id: row.id, - page_id: row.page_id, - block_order: row.block_order, - title: row.title, - chart_type: row.chart_type, - series, - config: row.config, - is_active: row.is_active, - created_at: row.created_at, - updated_at: row.updated_at, - })); - } - - // 获取设置块 - let settings_blocks = sqlx::query!( - r#" - SELECT id, page_id, block_order, title, category, editable, display_mode, is_active, created_at as "created_at: DateTime", updated_at as "updated_at: DateTime" - FROM settings_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order - "#, - page_id - ) - .fetch_all(&self.pool) - .await?; - - for row in settings_blocks { - blocks.push(Block::SettingsBlock(SettingsBlock { - id: row.id, - page_id: row.page_id, - block_order: row.block_order, - title: row.title, - category: row.category, - editable: row.editable, - display_mode: row.display_mode, - is_active: row.is_active, - created_at: row.created_at, - updated_at: row.updated_at, - })); - } - - // 获取表格块 - let table_blocks = sqlx::query!( - r#" - SELECT id, page_id, block_order, title, data_source, data_config, is_active, created_at as "created_at: DateTime", updated_at as "updated_at: DateTime" - FROM table_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order - "#, - page_id - ) - .fetch_all(&self.pool) - .await?; - - for row in table_blocks { - // 获取表格列 - let columns = sqlx::query!( - r#" - SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, "order" - FROM table_columns WHERE table_block_id = $1 ORDER BY "order" - "#, - row.id - ) - .fetch_all(&self.pool) - .await?; - - let table_columns = columns - .into_iter() - .map(|col| TableColumn { - id: col.id, - table_block_id: col.table_block_id, - name: col.name, - label: col.label, - data_type: col.data_type, - is_sortable: col.is_sortable, - is_filterable: col.is_filterable, - width: col.width, - order: col.order, - }) - .collect(); - - blocks.push(Block::TableBlock(TableBlock { - id: row.id, - page_id: row.page_id, - block_order: row.block_order, - title: row.title, - columns: table_columns, - data_source: row.data_source, - data_config: row.data_config, - is_active: row.is_active, - created_at: chrono::DateTime::from(row.created_at), - updated_at: chrono::DateTime::from(row.updated_at), - })); - } - - // 获取英雄块 - let hero_blocks = sqlx::query!( - r#" - SELECT id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active, created_at as "created_at: DateTime", updated_at as "updated_at: DateTime" - FROM hero_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order - "#, - page_id - ) - .fetch_all(&self.pool) - .await?; - - for row in hero_blocks { - blocks.push(Block::HeroBlock(HeroBlock { - id: row.id, - page_id: row.page_id, - block_order: row.block_order, - title: row.title, - subtitle: row.subtitle, - background_image: row.background_image, - background_color: row.background_color, - text_color: row.text_color, - cta_text: row.cta_text, - cta_link: row.cta_link, - is_active: row.is_active, - created_at: row.created_at, - updated_at: row.updated_at, - })); - } - - Ok(blocks) - } - - /// 创建文本块 - pub async fn create_text_block( - &self, - input: CreateTextBlockInput, - ) -> Result { - let result = sqlx::query_as!( - TextBlock, - r#" - INSERT INTO text_blocks (page_id, block_order, title, markdown, is_active) - VALUES ($1, $2, $3, $4, $5) - RETURNING id, page_id, block_order, title, markdown, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime" - "#, - input.page_id, - input.block_order, - input.title, - input.markdown, - input.is_active.unwrap_or(true) - ) - .fetch_one(&self.pool) - .await?; - - Ok(TextBlock { - id: result.id, - page_id: result.page_id, - block_order: result.block_order, - title: result.title, - markdown: result.markdown, - is_active: result.is_active, - created_at: chrono::DateTime::from(result.created_at), - updated_at: chrono::DateTime::from(result.updated_at), - }) - } - - /// 创建图表块 - pub async fn create_chart_block( - &self, - input: CreateChartBlockInput, - ) -> Result { - // 开始事务 - let mut tx = self.pool.begin().await?; - - // 创建图表块 - let chart_result = sqlx::query!( - r#" - INSERT INTO chart_blocks (page_id, block_order, title, chart_type, config, is_active) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id, page_id, block_order, title, chart_type, config, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime" - "#, - input.page_id, - input.block_order, - input.title, - input.chart_type, - input.config, - input.is_active.unwrap_or(true) - ) - .fetch_one(&mut *tx) - .await?; - - // 创建数据点 - for data_point in input.series { - sqlx::query!( - r#" - INSERT INTO data_points (chart_block_id, x, y, label, color) - VALUES ($1, $2, $3, $4, $5) - "#, - chart_result.id, - data_point.x, - data_point.y, - data_point.label, - data_point.color - ) - .execute(&mut *tx) - .await?; - } - - // 提交事务 - tx.commit().await?; - - // 获取完整的数据点 - let data_points = sqlx::query!( - r#" - SELECT id, chart_block_id, x, y, label, color - FROM data_points WHERE chart_block_id = $1 ORDER BY x - "#, - chart_result.id - ) - .fetch_all(&self.pool) - .await?; - - let series = data_points - .into_iter() - .map(|dp| DataPoint { - id: dp.id, - chart_block_id: dp.chart_block_id, - x: dp.x, - y: dp.y, - label: dp.label, - color: dp.color, - }) - .collect(); - - Ok(ChartBlock { - id: chart_result.id, - page_id: chart_result.page_id, - block_order: chart_result.block_order, - title: chart_result.title, - chart_type: chart_result.chart_type, - series, - config: chart_result.config, - is_active: chart_result.is_active, - created_at: chrono::DateTime::from(chart_result.created_at), - updated_at: chrono::DateTime::from(chart_result.updated_at), - }) - } - - /// 创建设置块 - pub async fn create_settings_block( - &self, - input: CreateSettingsBlockInput, - ) -> Result { - let result = sqlx::query_as!( - SettingsBlock, - r#" - INSERT INTO settings_blocks (page_id, block_order, title, category, editable, display_mode, is_active) - VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime" - "#, - input.page_id, - input.block_order, - input.title, - input.category, - input.editable, - input.display_mode, - input.is_active.unwrap_or(true) - ) - .fetch_one(&self.pool) - .await?; - - Ok(SettingsBlock { - id: result.id, - page_id: result.page_id, - block_order: result.block_order, - title: result.title, - category: result.category, - editable: result.editable, - display_mode: result.display_mode, - is_active: result.is_active, - created_at: chrono::DateTime::from(result.created_at), - updated_at: chrono::DateTime::from(result.updated_at), - }) - } - - /// 删除页面 - pub async fn delete_page(&self, page_id: Uuid) -> Result<(), PageBlockError> { - // 开始事务 - let mut tx = self.pool.begin().await?; - - // 删除所有相关的块 - sqlx::query!("DELETE FROM text_blocks WHERE page_id = $1", page_id) - .execute(&mut *tx) - .await?; - - sqlx::query!("DELETE FROM chart_blocks WHERE page_id = $1", page_id) - .execute(&mut *tx) - .await?; - - sqlx::query!("DELETE FROM data_points WHERE chart_block_id IN (SELECT id FROM chart_blocks WHERE page_id = $1)", page_id) - .execute(&mut *tx) - .await?; - - sqlx::query!("DELETE FROM settings_blocks WHERE page_id = $1", page_id) - .execute(&mut *tx) - .await?; - - sqlx::query!("DELETE FROM table_blocks WHERE page_id = $1", page_id) - .execute(&mut *tx) - .await?; - - sqlx::query!("DELETE FROM table_columns WHERE table_block_id IN (SELECT id FROM table_blocks WHERE page_id = $1)", page_id) - .execute(&mut *tx) - .await?; - - sqlx::query!("DELETE FROM hero_blocks WHERE page_id = $1", page_id) - .execute(&mut *tx) - .await?; - - // 删除页面 - sqlx::query!("DELETE FROM pages WHERE id = $1", page_id) - .execute(&mut *tx) - .await?; - - // 提交事务 - tx.commit().await?; - - Ok(()) - } - - /// 更新页面 - pub async fn update_page( - &self, - page_id: Uuid, - input: UpdatePageInput, - user_id: Uuid, - ) -> Result { - let result = sqlx::query_as!( - Page, - r#" - UPDATE pages - SET title = COALESCE($1, title), - slug = COALESCE($2, slug), - description = COALESCE($3, description), - is_active = COALESCE($4, is_active), - updated_at = NOW(), - updated_by = $5 - WHERE id = $6 - RETURNING id, title, slug, description, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - "#, - input.title, - input.slug, - input.description, - input.is_active, - user_id, - page_id - ) - .fetch_optional(&self.pool) - .await?; - - match result { - Some(result) => Ok(Page { - id: result.id, - title: result.title, - slug: result.slug, - description: result.description, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - created_by: result.created_by, - updated_by: result.updated_by, - }), - None => Err(PageBlockError::NotFound(format!("页面 {} 未找到", page_id))), - } - } - - /// 更新文本块 - pub async fn update_text_block( - &self, - block_id: Uuid, - input: UpdateTextBlockInput, - ) -> Result { - let result = sqlx::query_as!( - TextBlock, - r#" - UPDATE text_blocks - SET title = COALESCE($1, title), - markdown = COALESCE($2, markdown), - block_order = COALESCE($3, block_order), - is_active = COALESCE($4, is_active), - updated_at = NOW() - WHERE id = $5 - RETURNING id, page_id, block_order, title, markdown, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime" - "#, - input.title, - input.markdown, - input.block_order, - input.is_active, - block_id - ) - .fetch_optional(&self.pool) - .await?; - - match result { - Some(result) => Ok(TextBlock { - id: result.id, - page_id: result.page_id, - block_order: result.block_order, - title: result.title, - markdown: result.markdown, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - }), - None => Err(PageBlockError::NotFound(format!( - "文本块 {} 未找到", - block_id - ))), - } - } - - /// 更新图表块 - pub async fn update_chart_block( - &self, - block_id: Uuid, - input: UpdateChartBlockInput, - ) -> Result { - // 开始事务 - let mut tx = self.pool.begin().await?; - - // 更新图表块基本信息 - let chart_result = sqlx::query!( - r#" - UPDATE chart_blocks - SET title = COALESCE($1, title), - chart_type = COALESCE($2, chart_type), - config = COALESCE($3, config), - block_order = COALESCE($4, block_order), - is_active = COALESCE($5, is_active), - updated_at = NOW() - WHERE id = $6 - RETURNING id, page_id, block_order, title, chart_type, config, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime" - "#, - input.title, - input.chart_type, - input.config, - input.block_order, - input.is_active, - block_id - ) - .fetch_optional(&mut *tx) - .await?; - - let chart_block = match chart_result { - Some(result) => result, - None => { - tx.rollback().await?; - return Err(PageBlockError::NotFound(format!( - "图表块 {} 未找到", - block_id - ))); - } - }; - - // 如果提供了新的数据系列,则更新数据点 - if let Some(series) = input.series { - // 删除旧的数据点 - sqlx::query!( - "DELETE FROM data_points WHERE chart_block_id = $1", - block_id - ) - .execute(&mut *tx) - .await?; - - // 插入新的数据点 - for data_point in series { - sqlx::query!( - r#" - INSERT INTO data_points (chart_block_id, x, y, label, color) - VALUES ($1, $2, $3, $4, $5) - "#, - block_id, - data_point.x, - data_point.y, - data_point.label, - data_point.color - ) - .execute(&mut *tx) - .await?; - } - } - - // 提交事务 - tx.commit().await?; - - // 获取完整的数据点 - let data_points = sqlx::query!( - r#" - SELECT id, chart_block_id, x, y, label, color - FROM data_points WHERE chart_block_id = $1 ORDER BY x - "#, - block_id - ) - .fetch_all(&self.pool) - .await?; - - let series = data_points - .into_iter() - .map(|dp| DataPoint { - id: dp.id, - chart_block_id: dp.chart_block_id, - x: dp.x, - y: dp.y, - label: dp.label, - color: dp.color, - }) - .collect(); - - Ok(ChartBlock { - id: chart_block.id, - page_id: chart_block.page_id, - block_order: chart_block.block_order, - title: chart_block.title, - chart_type: chart_block.chart_type, - series, - config: chart_block.config, - is_active: chart_block.is_active, - created_at: chart_block.created_at, - updated_at: chart_block.updated_at, - }) - } - - /// 更新设置块 - pub async fn update_settings_block( - &self, - block_id: Uuid, - input: UpdateSettingsBlockInput, - ) -> Result { - let result = sqlx::query_as!( - SettingsBlock, - r#" - UPDATE settings_blocks - SET title = COALESCE($1, title), - category = COALESCE($2, category), - editable = COALESCE($3, editable), - display_mode = COALESCE($4, display_mode), - block_order = COALESCE($5, block_order), - is_active = COALESCE($6, is_active), - updated_at = NOW() - WHERE id = $7 - RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime" - "#, - input.title, - input.category, - input.editable, - input.display_mode, - input.block_order, - input.is_active, - block_id - ) - .fetch_optional(&self.pool) - .await?; - - match result { - Some(result) => Ok(SettingsBlock { - id: result.id, - page_id: result.page_id, - block_order: result.block_order, - title: result.title, - category: result.category, - editable: result.editable, - display_mode: result.display_mode, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - }), - None => Err(PageBlockError::NotFound(format!( - "设置块 {} 未找到", - block_id - ))), - } - } - - /// 更新表格块 - pub async fn update_table_block( - &self, - block_id: Uuid, - input: UpdateTableBlockInput, - ) -> Result { - // 开始事务 - let mut tx = self.pool.begin().await?; - - // 创建临时结构体来接收更新后的基本信息 - #[derive(sqlx::FromRow)] - struct TableBlockUpdate { - id: Uuid, - page_id: Uuid, - block_order: i32, - title: Option, - data_source: String, - data_config: Option, - is_active: bool, - created_at: DateTime, - updated_at: DateTime, - } - - // 更新表格块基本信息 - let table_result = sqlx::query_as!( - TableBlockUpdate, - r#" - UPDATE table_blocks - SET title = COALESCE($1, title), - data_source = COALESCE($2, data_source), - data_config = COALESCE($3, data_config), - block_order = COALESCE($4, block_order), - is_active = COALESCE($5, is_active), - updated_at = NOW() - WHERE id = $6 - RETURNING id, page_id, block_order, title, data_source, data_config, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime" - "#, - input.title, - input.data_source, - input.data_config, - input.block_order, - input.is_active, - block_id - ) - .fetch_optional(&mut *tx) - .await?; - - let table_block = match table_result { - Some(result) => result, - None => { - tx.rollback().await?; - return Err(PageBlockError::NotFound(format!( - "表格块 {} 未找到", - block_id - ))); - } - }; - - // 如果提供了新的列配置,则更新列 - if let Some(columns) = input.columns { - // 删除旧的列 - sqlx::query!( - "DELETE FROM table_columns WHERE table_block_id = $1", - block_id - ) - .execute(&mut *tx) - .await?; - - // 插入新的列 - for column in columns { - sqlx::query!( - r#" - INSERT INTO table_columns (table_block_id, name, label, data_type, is_sortable, is_filterable, width, "order") - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - "#, - block_id, - column.name, - column.label, - column.data_type, - column.is_sortable, - column.is_filterable, - column.width, - column.order - ) - .execute(&mut *tx) - .await?; - } - } - - // 提交事务 - tx.commit().await?; - - // 获取完整的列配置 - let columns = sqlx::query!( - r#" - SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, "order" - FROM table_columns WHERE table_block_id = $1 ORDER BY "order" - "#, - block_id - ) - .fetch_all(&self.pool) - .await?; - - let table_columns = columns - .into_iter() - .map(|col| TableColumn { - id: col.id, - table_block_id: col.table_block_id, - name: col.name, - label: col.label, - data_type: col.data_type, - is_sortable: col.is_sortable, - is_filterable: col.is_filterable, - width: col.width, - order: col.order, - }) - .collect(); - - Ok(TableBlock { - id: table_block.id, - page_id: table_block.page_id, - block_order: table_block.block_order, - title: table_block.title, - columns: table_columns, - data_source: table_block.data_source, - data_config: table_block.data_config, - is_active: table_block.is_active, - created_at: table_block.created_at, - updated_at: table_block.updated_at, - }) - } - - /// 更新英雄块 - pub async fn update_hero_block( - &self, - block_id: Uuid, - input: UpdateHeroBlockInput, - ) -> Result { - let result = sqlx::query_as!( - HeroBlock, - r#" - UPDATE hero_blocks - SET title = COALESCE($1, title), - subtitle = COALESCE($2, subtitle), - background_image = COALESCE($3, background_image), - background_color = COALESCE($4, background_color), - text_color = COALESCE($5, text_color), - cta_text = COALESCE($6, cta_text), - cta_link = COALESCE($7, cta_link), - block_order = COALESCE($8, block_order), - is_active = COALESCE($9, is_active), - updated_at = NOW() - WHERE id = $10 - RETURNING id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime" - "#, - input.title, - input.subtitle, - input.background_image, - input.background_color, - input.text_color, - input.cta_text, - input.cta_link, - input.block_order, - input.is_active, - block_id - ) - .fetch_optional(&self.pool) - .await?; - - match result { - Some(result) => Ok(HeroBlock { - id: result.id, - page_id: result.page_id, - block_order: result.block_order, - title: result.title, - subtitle: result.subtitle, - background_image: result.background_image, - background_color: result.background_color, - text_color: result.text_color, - cta_text: result.cta_text, - cta_link: result.cta_link, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - }), - None => Err(PageBlockError::NotFound(format!( - "英雄块 {} 未找到", - block_id - ))), - } - } - - /// 删除文本块 - pub async fn delete_text_block(&self, block_id: Uuid) -> Result<(), PageBlockError> { - let result = sqlx::query!( - "DELETE FROM text_blocks WHERE id = $1 RETURNING id", - block_id - ) - .fetch_optional(&self.pool) - .await?; - - if result.is_none() { - return Err(PageBlockError::NotFound(format!( - "文本块 {} 未找到", - block_id - ))); - } - - Ok(()) - } - - /// 删除图表块 - pub async fn delete_chart_block(&self, block_id: Uuid) -> Result<(), PageBlockError> { - // 开始事务 - let mut tx = self.pool.begin().await?; - - // 删除数据点 - sqlx::query!( - "DELETE FROM data_points WHERE chart_block_id = $1", - block_id - ) - .execute(&mut *tx) - .await?; - - // 删除图表块 - let result = sqlx::query!( - "DELETE FROM chart_blocks WHERE id = $1 RETURNING id", - block_id - ) - .fetch_optional(&mut *tx) - .await?; - - if result.is_none() { - tx.rollback().await?; - return Err(PageBlockError::NotFound(format!( - "图表块 {} 未找到", - block_id - ))); - } - - // 提交事务 - tx.commit().await?; - Ok(()) - } - - /// 删除设置块 - pub async fn delete_settings_block(&self, block_id: Uuid) -> Result<(), PageBlockError> { - let result = sqlx::query!( - "DELETE FROM settings_blocks WHERE id = $1 RETURNING id", - block_id - ) - .fetch_optional(&self.pool) - .await?; - - if result.is_none() { - return Err(PageBlockError::NotFound(format!( - "设置块 {} 未找到", - block_id - ))); - } - - Ok(()) - } - - /// 删除表格块 - pub async fn delete_table_block(&self, block_id: Uuid) -> Result<(), PageBlockError> { - // 开始事务 - let mut tx = self.pool.begin().await?; - - // 删除表格列 - sqlx::query!( - "DELETE FROM table_columns WHERE table_block_id = $1", - block_id - ) - .execute(&mut *tx) - .await?; - - // 删除表格块 - let result = sqlx::query!( - "DELETE FROM table_blocks WHERE id = $1 RETURNING id", - block_id - ) - .fetch_optional(&mut *tx) - .await?; - - if result.is_none() { - tx.rollback().await?; - return Err(PageBlockError::NotFound(format!( - "表格块 {} 未找到", - block_id - ))); - } - - // 提交事务 - tx.commit().await?; - Ok(()) - } - - /// 删除英雄块 - pub async fn delete_hero_block(&self, block_id: Uuid) -> Result<(), PageBlockError> { - let result = sqlx::query!( - "DELETE FROM hero_blocks WHERE id = $1 RETURNING id", - block_id - ) - .fetch_optional(&self.pool) - .await?; - - if result.is_none() { - return Err(PageBlockError::NotFound(format!( - "英雄块 {} 未找到", - block_id - ))); - } - - Ok(()) - } - - /// 重新排序页面块 - pub async fn reorder_page_blocks( - &self, - page_id: Uuid, - block_orders: Vec<(Uuid, i32)>, - ) -> Result<(), PageBlockError> { - // 开始事务 - let mut tx = self.pool.begin().await?; - - for (block_id, new_order) in block_orders { - // 尝试更新各种类型的块 - let mut updated = false; - - // 更新文本块 - let result = sqlx::query!( - "UPDATE text_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - new_order, block_id, page_id - ) - .fetch_optional(&mut *tx) - .await?; - if result.is_some() { - updated = true; - continue; - } - - // 更新图表块 - let result = sqlx::query!( - "UPDATE chart_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - new_order, block_id, page_id - ) - .fetch_optional(&mut *tx) - .await?; - if result.is_some() { - updated = true; - continue; - } - - // 更新设置块 - let result = sqlx::query!( - "UPDATE settings_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - new_order, block_id, page_id - ) - .fetch_optional(&mut *tx) - .await?; - if result.is_some() { - updated = true; - continue; - } - - // 更新表格块 - let result = sqlx::query!( - "UPDATE table_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - new_order, block_id, page_id - ) - .fetch_optional(&mut *tx) - .await?; - if result.is_some() { - updated = true; - continue; - } - - // 更新英雄块 - let result = sqlx::query!( - "UPDATE hero_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id", - new_order, block_id, page_id - ) - .fetch_optional(&mut *tx) - .await?; - if result.is_some() { - updated = true; - continue; - } - - if !updated { - tx.rollback().await?; - return Err(PageBlockError::NotFound(format!( - "块 {} 未找到或不属于页面 {}", - block_id, page_id - ))); - } - } - - // 提交事务 - tx.commit().await?; - Ok(()) - } - - /// 获取页面的统计信息 - pub async fn get_page_stats(&self, page_id: Uuid) -> Result { - let text_count = sqlx::query!( - "SELECT COUNT(*) as count FROM text_blocks WHERE page_id = $1 AND is_active = true", - page_id - ) - .fetch_one(&self.pool) - .await? - .count; - - let chart_count = sqlx::query!( - "SELECT COUNT(*) as count FROM chart_blocks WHERE page_id = $1 AND is_active = true", - page_id - ) - .fetch_one(&self.pool) - .await? - .count; - - let settings_count = sqlx::query!( - "SELECT COUNT(*) as count FROM settings_blocks WHERE page_id = $1 AND is_active = true", - page_id - ) - .fetch_one(&self.pool) - .await? - .count; - - let table_count = sqlx::query!( - "SELECT COUNT(*) as count FROM table_blocks WHERE page_id = $1 AND is_active = true", - page_id - ) - .fetch_one(&self.pool) - .await? - .count; - - let hero_count = sqlx::query!( - "SELECT COUNT(*) as count FROM hero_blocks WHERE page_id = $1 AND is_active = true", - page_id - ) - .fetch_one(&self.pool) - .await? - .count; - - Ok(PageStats { - text_blocks: text_count.unwrap_or(0) as i32, - chart_blocks: chart_count.unwrap_or(0) as i32, - settings_blocks: settings_count.unwrap_or(0) as i32, - table_blocks: table_count.unwrap_or(0) as i32, - hero_blocks: hero_count.unwrap_or(0) as i32, - total_blocks: (text_count.unwrap_or(0) - + chart_count.unwrap_or(0) - + settings_count.unwrap_or(0) - + table_count.unwrap_or(0) - + hero_count.unwrap_or(0)) as i32, - }) - } - - /// 检查页面slug是否唯一 - pub async fn is_slug_unique( - &self, - slug: &str, - exclude_id: Option, - ) -> Result { - let count = match exclude_id { - Some(id) => { - sqlx::query!( - "SELECT COUNT(*) as count FROM pages WHERE slug = $1 AND id != $2", - slug, - id - ) - .fetch_one(&self.pool) - .await? - .count - } - None => { - sqlx::query!("SELECT COUNT(*) as count FROM pages WHERE slug = $1", slug) - .fetch_one(&self.pool) - .await? - .count - } - }; - - Ok(count.unwrap_or(0) == 0) - } - - /// 批量获取页面 - pub async fn get_pages_by_ids(&self, page_ids: &[Uuid]) -> Result, PageBlockError> { - if page_ids.is_empty() { - return Ok(Vec::new()); - } - - let mut pages = Vec::new(); - for page_id in page_ids { - match self.get_page_by_id(*page_id).await { - Ok(page) => pages.push(page), - Err(PageBlockError::NotFound(_)) => continue, // 跳过不存在的页面 - Err(e) => return Err(e), - } - } - - Ok(pages) - } - - /// 搜索页面 - pub async fn search_pages( - &self, - query: &str, - limit: Option, - ) -> Result, PageBlockError> { - let limit = limit.unwrap_or(10); - - let rows = sqlx::query_as!( - Page, - r#" - SELECT id, title, slug, description, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - FROM pages - WHERE is_active = true - AND (title ILIKE $1 OR description ILIKE $1 OR slug ILIKE $1) - ORDER BY - CASE - WHEN title ILIKE $1 THEN 1 - WHEN slug ILIKE $1 THEN 2 - ELSE 3 - END, - updated_at DESC - LIMIT $2 - "#, - format!("%{}%", query), - limit - ) - .fetch_all(&self.pool) - .await?; - - let pages = rows - .into_iter() - .map(|result| Page { - id: result.id, - title: result.title, - slug: result.slug, - description: result.description, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - created_by: result.created_by, - updated_by: result.updated_by, - }) - .collect(); - - Ok(pages) - } - - /// 根据配置分类获取页面 - pub async fn get_page_by_category( - &self, - category: &str, - ) -> Result, PageBlockError> { - let result = sqlx::query_as!( - Page, - r#" - SELECT id, title, slug, description, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - FROM pages WHERE slug = $1 AND is_active = true - "#, - format!("{}-settings", category) - ) - .fetch_optional(&self.pool) - .await?; - - match result { - Some(result) => Ok(Some(Page { - id: result.id, - title: result.title, - slug: result.slug, - description: result.description, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - created_by: result.created_by, - updated_by: result.updated_by, - })), - None => Ok(None), - } - } - - /// 根据配置分类获取页面和统计信息 - pub async fn get_category_page_with_stats( - &self, - category: &str, - ) -> Result<(Option, i32, i32, i32), PageBlockError> { - // 获取页面信息 - let page = self.get_page_by_category(category).await?; - - // 获取该分类下的配置项统计 - let stats_result = sqlx::query!( - r#" - SELECT - COUNT(*) as total_count, - COUNT(CASE WHEN is_system = true THEN 1 END) as system_count, - COUNT(CASE WHEN is_editable = true THEN 1 END) as editable_count - FROM settings - WHERE category = $1 - "#, - category - ) - .fetch_one(&self.pool) - .await?; - - let total_count = stats_result.total_count.unwrap_or(0) as i32; - let system_count = stats_result.system_count.unwrap_or(0) as i32; - let editable_count = stats_result.editable_count.unwrap_or(0) as i32; - - Ok((page, total_count, system_count, editable_count)) - } - - /// 获取所有配置分类页面 - pub async fn get_all_category_pages(&self) -> Result, PageBlockError> { - let result = sqlx::query_as!( - Page, - r#" - SELECT id, title, slug, description, is_active, - created_at as "created_at: DateTime", - updated_at as "updated_at: DateTime", - created_by, updated_by - FROM pages WHERE slug LIKE '%-settings' AND is_active = true - ORDER BY title - "# - ) - .fetch_all(&self.pool) - .await?; - - let pages = result - .into_iter() - .map(|result| Page { - id: result.id, - title: result.title, - slug: result.slug, - description: result.description, - is_active: result.is_active, - created_at: result.created_at, - updated_at: result.updated_at, - created_by: result.created_by, - updated_by: result.updated_by, - }) - .collect(); - - Ok(pages) - } -} - -// 定义表结构常量 -mod Pages { - use sea_query::Iden; - #[derive(Iden)] - pub enum Pages { - #[iden = "pages"] - Table, - Id, - Title, - Slug, - Description, - IsActive, - CreatedAt, - UpdatedAt, - CreatedBy, - UpdatedBy, - } -} diff --git a/src/services/user_service.rs b/src/services/user_service.rs index 38a066b..587dd96 100644 --- a/src/services/user_service.rs +++ b/src/services/user_service.rs @@ -1,5 +1,7 @@ -use argon2::password_hash::{rand_core::OsRng, SaltString}; -use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHash, PasswordHasher, PasswordVerifier, +}; use async_graphql::{Error, Result}; use sea_query::{Expr, PostgresQueryBuilder}; use sea_query_binder::SqlxBinder; @@ -8,10 +10,9 @@ use tracing::info; use uuid::Uuid; use crate::auth::Claims; -use crate::graphql::types::{ - CreateUserInput, LoginInput, LoginResponse, RegisterInput, UserInfoRespnose, -}; -use crate::models::user::{Role, User}; +use crate::graphql::types::users::LoginResponse; +use crate::models::user::*; +use crate::services::casbin_service::CasbinService; use crate::services::invite_code_service::InviteCodeService; use crate::services::system_config_service::SystemConfigService; @@ -19,16 +20,51 @@ use crate::services::system_config_service::SystemConfigService; pub struct UserService { pool: PgPool, jwt_secret: String, + casbin_service: Option, } impl UserService { pub fn new(pool: PgPool, jwt_secret: String) -> Self { - Self { pool, jwt_secret } + Self { + pool, + jwt_secret, + casbin_service: None, + } + } + + pub fn with_casbin(mut self, casbin_service: CasbinService) -> Self { + self.casbin_service = Some(casbin_service); + self + } + + /// 获取用户的组(角色) + pub async fn get_user_groups(&self, user_id: &Uuid) -> Result> { + if let Some(casbin_service) = &self.casbin_service { + casbin_service + .get_user_roles(&user_id.to_string()) + .await + .map_err(|e| Error::new(format!("Failed to get user groups: {}", e))) + } else { + Err(Error::new("Casbin service not initialized")) + } + } + + /// 获取用户信息(包含组信息) + pub async fn get_user_with_groups(&self, user_id: Uuid) -> Result> { + let user = self.get_user_by_id(user_id).await?; + + if let Some(user) = user { + let groups = self.get_user_groups(&user.id).await?; + + Ok(Some(UserWithGroups { user, groups })) + } else { + Ok(None) + } } pub async fn register(&self, input: RegisterInput) -> Result { let password_hash = self.hash_password(&input.password)?; - let role = input.role.unwrap_or(Role::User); + let role = Role::User; // Use transaction to ensure data consistency let mut tx = self @@ -113,7 +149,7 @@ impl UserService { pub async fn create_user(&self, input: CreateUserInput) -> Result { let password_hash = self.hash_password(&input.password)?; - let role = input.role.unwrap_or(Role::User); + let role = Role::User; // Create user first let user = sqlx::query_as!( @@ -160,7 +196,10 @@ impl UserService { .map_err(|e| Error::new(format!("Database error: {}", e)))? .ok_or_else(|| Error::new("Invalid credentials"))?; - if !self.verify_password(&input.password, &user.password_hash)? { + if !self.verify_password( + &input.password, + &user.password_hash.as_ref().as_ref().unwrap(), + )? { return Err(Error::new("Invalid credentials")); } @@ -197,7 +236,7 @@ impl UserService { sort_by: String, sort_order: String, filter: Option, - ) -> Result> { + ) -> Result> { use crate::models::user::Users; use sea_query::{Expr, Order, Query}; let sort_by = Users::try_from(sort_by)?; @@ -213,6 +252,8 @@ impl UserService { Users::Username, Users::Email, Users::Role, + Users::PasswordHash, + Users::InviteCodeId, Users::IsActivate, Users::CreatedAt, Users::UpdatedAt, @@ -224,10 +265,7 @@ impl UserService { .offset(offset) .build_sqlx(PostgresQueryBuilder); - info!("sql: {:?}", sql); - info!("values: {:?}", values); - - let users = sqlx::query_as_with::<_, crate::models::user::UserInfoRow, _>(&sql, values) + let users = sqlx::query_as_with::<_, crate::models::user::User, _>(&sql, values) .fetch_all(&self.pool) .await .map_err(|e| Error::new(format!("Database error: {}", e)))?; @@ -235,6 +273,27 @@ impl UserService { Ok(users) } + pub async fn get_all_users_with_groups( + &self, + offset: u64, + limit: u64, + sort_by: String, + sort_order: String, + filter: Option, + ) -> Result> { + let users = self + .get_all_users(offset, limit, sort_by, sort_order, filter) + .await?; + + let mut users_with_groups = Vec::new(); + + for user in users { + let groups = self.get_user_groups(&user.id).await.unwrap_or_default(); + users_with_groups.push(UserWithGroups { user, groups }); + } + Ok(users_with_groups) + } + pub async fn initialize_admin( &self, username: String, @@ -352,9 +411,8 @@ impl UserService { .await .map_err(|e| Error::new(format!("Database error: {}", e)))?; - // 并行获取用户列表 - let users = self - .get_all_users(offset, limit, sort_by, sort_order, filter) + let users_with_groups = self + .get_all_users_with_groups(offset, limit, sort_by, sort_order, filter) .await?; Ok(UserInfoRespnose { @@ -363,7 +421,7 @@ impl UserService { total_inactive_users: stats.total_inactive_users.unwrap_or(0), total_admin_users: stats.total_admin_users.unwrap_or(0), total_user_users: stats.total_user_users.unwrap_or(0), - users, + users: users_with_groups, }) } }