diff --git a/.sqlx/query-0a18d6fc5b51dc801d8e674590b2d609e107196eca233278f827f11c242b6e38.json b/.sqlx/query-0a18d6fc5b51dc801d8e674590b2d609e107196eca233278f827f11c242b6e38.json new file mode 100644 index 0000000..c4f6546 --- /dev/null +++ b/.sqlx/query-0a18d6fc5b51dc801d8e674590b2d609e107196eca233278f827f11c242b6e38.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT COUNT(id) as id_count, SUM(download_count) as download_sum\n FROM (\n select m.id, m.download_count, row_number() over(partition by m.id) rn\n FROM mods m\n INNER JOIN mod_versions mv ON mv.mod_id = m.id\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id\n WHERE mvs.status = 'accepted'\n ) q\n WHERE q.rn = 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id_count", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "download_sum", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null, + null + ] + }, + "hash": "0a18d6fc5b51dc801d8e674590b2d609e107196eca233278f827f11c242b6e38" +} diff --git a/.sqlx/query-f10845904d7f6db087feb5065d7282e398248fd4bce5c31cdfe19168ce19376f.json b/.sqlx/query-1ed87bd1d14f9c1febbb2a2a2d11eb3d413d28283d97c0b9bfca6f4ca3c80256.json similarity index 78% rename from .sqlx/query-f10845904d7f6db087feb5065d7282e398248fd4bce5c31cdfe19168ce19376f.json rename to .sqlx/query-1ed87bd1d14f9c1febbb2a2a2d11eb3d413d28283d97c0b9bfca6f4ca3c80256.json index cc68be1..5651f29 100644 --- a/.sqlx/query-f10845904d7f6db087feb5065d7282e398248fd4bce5c31cdfe19168ce19376f.json +++ b/.sqlx/query-1ed87bd1d14f9c1febbb2a2a2d11eb3d413d28283d97c0b9bfca6f4ca3c80256.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n id,\n username,\n display_name,\n verified,\n admin,\n github_user_id as github_id\n FROM developers\n WHERE (\n ($1 = '' OR username = $1)\n OR ($2 = '' OR display_name ILIKE $2)\n )\n GROUP BY id\n LIMIT $3\n OFFSET $4", + "query": "SELECT\n id,\n username,\n display_name,\n verified,\n admin,\n github_user_id as github_id\n FROM developers\n WHERE (\n ($1::text IS NULL OR username = $1)\n OR ($1::text IS NULL OR display_name ILIKE '%' || $1 || '%')\n )\n GROUP BY id\n LIMIT $2\n OFFSET $3", "describe": { "columns": [ { @@ -36,7 +36,6 @@ ], "parameters": { "Left": [ - "Text", "Text", "Int8", "Int8" @@ -51,5 +50,5 @@ false ] }, - "hash": "f10845904d7f6db087feb5065d7282e398248fd4bce5c31cdfe19168ce19376f" + "hash": "1ed87bd1d14f9c1febbb2a2a2d11eb3d413d28283d97c0b9bfca6f4ca3c80256" } diff --git a/.sqlx/query-470fe23e11b98365be7eb21c56cc6021a675d8270dd16425a9b772f0e7d55009.json b/.sqlx/query-470fe23e11b98365be7eb21c56cc6021a675d8270dd16425a9b772f0e7d55009.json deleted file mode 100644 index e25c148..0000000 --- a/.sqlx/query-470fe23e11b98365be7eb21c56cc6021a675d8270dd16425a9b772f0e7d55009.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT COUNT(id), SUM(download_count)\n FROM (\n select m.id, m.download_count, row_number() over(partition by m.id) rn\n FROM mods m\n INNER JOIN mod_versions mv ON mv.mod_id = m.id\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id\n WHERE mvs.status = 'accepted'\n ) q\n WHERE q.rn = 1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "sum", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - null, - null - ] - }, - "hash": "470fe23e11b98365be7eb21c56cc6021a675d8270dd16425a9b772f0e7d55009" -} diff --git a/.sqlx/query-7d171a79184b68e491ed200d347d6b412b621e2cc3a5b63481643d7e90d05566.json b/.sqlx/query-7d171a79184b68e491ed200d347d6b412b621e2cc3a5b63481643d7e90d05566.json new file mode 100644 index 0000000..8e3efe6 --- /dev/null +++ b/.sqlx/query-7d171a79184b68e491ed200d347d6b412b621e2cc3a5b63481643d7e90d05566.json @@ -0,0 +1,73 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT mgv.id, mgv.mod_id, mgv.gd AS \"gd: _\", mgv.platform as \"platform: _\"\n FROM mod_gd_versions mgv\n WHERE mgv.mod_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "gd: _", + "type_info": { + "Custom": { + "name": "gd_version", + "kind": { + "Enum": [ + "*", + "2.113", + "2.200", + "2.204", + "2.205", + "2.206", + "2.207", + "2.2071", + "2.2072", + "2.2073", + "2.2074" + ] + } + } + } + }, + { + "ordinal": 3, + "name": "platform: _", + "type_info": { + "Custom": { + "name": "gd_ver_platform", + "kind": { + "Enum": [ + "android32", + "android64", + "ios", + "mac-arm", + "mac-intel", + "win" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "7d171a79184b68e491ed200d347d6b412b621e2cc3a5b63481643d7e90d05566" +} diff --git a/.sqlx/query-c1be1c8dbe7dc54c2d74640bf61485859a51ad1ae6bae8e6ac91ad62360d0093.json b/.sqlx/query-83f3fb02300d940b3955b97b724216460bd7248b6cba2da9aaa0ade8ec7bf967.json similarity index 73% rename from .sqlx/query-c1be1c8dbe7dc54c2d74640bf61485859a51ad1ae6bae8e6ac91ad62360d0093.json rename to .sqlx/query-83f3fb02300d940b3955b97b724216460bd7248b6cba2da9aaa0ade8ec7bf967.json index 6b85966..5be1a34 100644 --- a/.sqlx/query-c1be1c8dbe7dc54c2d74640bf61485859a51ad1ae6bae8e6ac91ad62360d0093.json +++ b/.sqlx/query-83f3fb02300d940b3955b97b724216460bd7248b6cba2da9aaa0ade8ec7bf967.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT mv.id, mv.name, mv.description, mv.version, \n mv.download_link, mv.download_count,\n mv.hash,\n format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as \"geode!: _\",\n mv.early_load, mv.api,\n mv.created_at, mv.updated_at,\n mv.mod_id, mvs.status as \"status: _\", mvs.info\n FROM mod_versions mv\n INNER JOIN mods m ON m.id = mv.mod_id\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id \n WHERE mv.mod_id = $1 AND mv.version = $2 \n AND (mvs.status = 'accepted' OR $3 = false)", + "query": "SELECT mv.id, mv.name, mv.description, mv.version,\n mv.download_link, mv.download_count,\n mv.hash,\n format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as \"geode!: _\",\n mv.early_load, mv.api,\n mv.created_at, mv.updated_at,\n mv.mod_id, mvs.status as \"status: _\", mvs.info\n FROM mod_versions mv\n INNER JOIN mods m ON m.id = mv.mod_id\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id\n WHERE mv.mod_id = $1 AND mv.version = $2\n AND (mvs.status = 'accepted' OR $3 = false)", "describe": { "columns": [ { @@ -116,5 +116,5 @@ true ] }, - "hash": "c1be1c8dbe7dc54c2d74640bf61485859a51ad1ae6bae8e6ac91ad62360d0093" + "hash": "83f3fb02300d940b3955b97b724216460bd7248b6cba2da9aaa0ade8ec7bf967" } diff --git a/.sqlx/query-acd5392daf7d6e76a3f76bae9051502cc4d945731f5593b430e250019c7e8a3e.json b/.sqlx/query-94a9f2bc14d5a6c21eb7f638e5d70bcdc7112d9ad0d153aaad6bf7b00e27d74a.json similarity index 89% rename from .sqlx/query-acd5392daf7d6e76a3f76bae9051502cc4d945731f5593b430e250019c7e8a3e.json rename to .sqlx/query-94a9f2bc14d5a6c21eb7f638e5d70bcdc7112d9ad0d153aaad6bf7b00e27d74a.json index 3074da9..3226eb2 100644 --- a/.sqlx/query-acd5392daf7d6e76a3f76bae9051502cc4d945731f5593b430e250019c7e8a3e.json +++ b/.sqlx/query-94a9f2bc14d5a6c21eb7f638e5d70bcdc7112d9ad0d153aaad6bf7b00e27d74a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n\t\t\t\tmac as \"mac: _\", win as \"win: _\", android as \"android: _\", ios as \"ios: _\",\n\t\t\t\ttag, created_at, commit_hash, prerelease\n\t\t\tFROM geode_versions\n\t\t\t\tWHERE tag = $1", + "query": "SELECT\n\t\t\t\t mac as \"mac: _\", win as \"win: _\", android as \"android: _\", ios as \"ios: _\",\n\t\t\t\t tag, created_at, commit_hash, prerelease\n\t\t\t FROM geode_versions\n\t\t\t\t WHERE tag = $1", "describe": { "columns": [ { @@ -136,5 +136,5 @@ false ] }, - "hash": "acd5392daf7d6e76a3f76bae9051502cc4d945731f5593b430e250019c7e8a3e" + "hash": "94a9f2bc14d5a6c21eb7f638e5d70bcdc7112d9ad0d153aaad6bf7b00e27d74a" } diff --git a/.sqlx/query-9e0a23498d6500f6b5479b5edf560ba083750b5ed1c229b507e94e448a0a0374.json b/.sqlx/query-9e0a23498d6500f6b5479b5edf560ba083750b5ed1c229b507e94e448a0a0374.json deleted file mode 100644 index 16c1b62..0000000 --- a/.sqlx/query-9e0a23498d6500f6b5479b5edf560ba083750b5ed1c229b507e94e448a0a0374.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO github_loader_release_stats (total_download_count, latest_loader_version)\n VALUES ($1, $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - }, - "nullable": [] - }, - "hash": "9e0a23498d6500f6b5479b5edf560ba083750b5ed1c229b507e94e448a0a0374" -} diff --git a/.sqlx/query-70db4ee34c2c0a34c323d9bab8fb427328e9fae10c8e8ff3a12a53578f636c8a.json b/.sqlx/query-a6c2a86e7f95612a9a2fdcc5168f9ab2485fc07b1b3d47f5327b91c6fcb9a653.json similarity index 83% rename from .sqlx/query-70db4ee34c2c0a34c323d9bab8fb427328e9fae10c8e8ff3a12a53578f636c8a.json rename to .sqlx/query-a6c2a86e7f95612a9a2fdcc5168f9ab2485fc07b1b3d47f5327b91c6fcb9a653.json index 6c92ccf..6a75f9e 100644 --- a/.sqlx/query-70db4ee34c2c0a34c323d9bab8fb427328e9fae10c8e8ff3a12a53578f636c8a.json +++ b/.sqlx/query-a6c2a86e7f95612a9a2fdcc5168f9ab2485fc07b1b3d47f5327b91c6fcb9a653.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT mgv.id, mgv.mod_id, mgv.gd AS \"gd: _\", mgv.platform as \"platform: _\" FROM mod_gd_versions mgv WHERE mgv.mod_id = $1", + "query": "SELECT\n id, mod_id, gd as \"gd: _\", platform as \"platform: _\"\n FROM mod_gd_versions\n WHERE mod_id = ANY($1)", "describe": { "columns": [ { @@ -59,7 +59,7 @@ ], "parameters": { "Left": [ - "Int4" + "Int4Array" ] }, "nullable": [ @@ -69,5 +69,5 @@ false ] }, - "hash": "70db4ee34c2c0a34c323d9bab8fb427328e9fae10c8e8ff3a12a53578f636c8a" + "hash": "a6c2a86e7f95612a9a2fdcc5168f9ab2485fc07b1b3d47f5327b91c6fcb9a653" } diff --git a/.sqlx/query-fd1e2a86b822e0d48282dd3c64be3b75ee73c80f49c0d8e750e014da9bdd1ffa.json b/.sqlx/query-c376c807583ad0bec9f6abe17ba00ce8d2cf76047bb0d1bb97b6e20c8d527224.json similarity index 60% rename from .sqlx/query-fd1e2a86b822e0d48282dd3c64be3b75ee73c80f49c0d8e750e014da9bdd1ffa.json rename to .sqlx/query-c376c807583ad0bec9f6abe17ba00ce8d2cf76047bb0d1bb97b6e20c8d527224.json index fae0459..fce49e2 100644 --- a/.sqlx/query-fd1e2a86b822e0d48282dd3c64be3b75ee73c80f49c0d8e750e014da9bdd1ffa.json +++ b/.sqlx/query-c376c807583ad0bec9f6abe17ba00ce8d2cf76047bb0d1bb97b6e20c8d527224.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT COUNT(id)\n FROM developers\n WHERE (\n ($1 = '' OR username = $1)\n OR ($2 = '' OR display_name ILIKE $2)\n )", + "query": "SELECT COUNT(id)\n FROM developers\n WHERE (\n ($1::text IS NULL OR username = $1)\n OR ($1::text IS NULL OR display_name ILIKE '%' || $1 || '%')\n )", "describe": { "columns": [ { @@ -11,7 +11,6 @@ ], "parameters": { "Left": [ - "Text", "Text" ] }, @@ -19,5 +18,5 @@ null ] }, - "hash": "fd1e2a86b822e0d48282dd3c64be3b75ee73c80f49c0d8e750e014da9bdd1ffa" + "hash": "c376c807583ad0bec9f6abe17ba00ce8d2cf76047bb0d1bb97b6e20c8d527224" } diff --git a/.sqlx/query-e3f31899201fa6b9ce4da995474486e2965c5bdcdc2bc06942ad78219c9b2339.json b/.sqlx/query-e3f31899201fa6b9ce4da995474486e2965c5bdcdc2bc06942ad78219c9b2339.json new file mode 100644 index 0000000..9ea3f46 --- /dev/null +++ b/.sqlx/query-e3f31899201fa6b9ce4da995474486e2965c5bdcdc2bc06942ad78219c9b2339.json @@ -0,0 +1,118 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT DISTINCT\n mv.name, mv.id, mv.description, mv.version, mv.download_link, mv.hash,\n format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as \"geode!: _\",\n mv.download_count, mv.early_load, mv.api, mv.mod_id, mv.created_at, mv.updated_at,\n 'pending'::mod_version_status as \"status!: _\", NULL as info\n FROM mod_versions mv\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id\n WHERE mvs.status = 'pending'\n AND mv.mod_id = ANY($1)\n ORDER BY mv.id DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "download_link", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "hash", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "geode!: _", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "download_count", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "early_load", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "api", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "mod_id", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 12, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 13, + "name": "status!: _", + "type_info": { + "Custom": { + "name": "mod_version_status", + "kind": { + "Enum": [ + "pending", + "rejected", + "accepted", + "unlisted" + ] + } + } + } + }, + { + "ordinal": 14, + "name": "info", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "TextArray" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + null, + false, + false, + false, + false, + true, + true, + null, + null + ] + }, + "hash": "e3f31899201fa6b9ce4da995474486e2965c5bdcdc2bc06942ad78219c9b2339" +} diff --git a/.sqlx/query-6e713aab78ba4c3c8c7f6ffcb35290f10c23e7f46964dcf50fc9305711e59632.json b/.sqlx/query-e76119227a3fcb00348279467161b081128d2a2cf67057da0e4b726463ceb40a.json similarity index 63% rename from .sqlx/query-6e713aab78ba4c3c8c7f6ffcb35290f10c23e7f46964dcf50fc9305711e59632.json rename to .sqlx/query-e76119227a3fcb00348279467161b081128d2a2cf67057da0e4b726463ceb40a.json index 32fc8a7..2e186ba 100644 --- a/.sqlx/query-6e713aab78ba4c3c8c7f6ffcb35290f10c23e7f46964dcf50fc9305711e59632.json +++ b/.sqlx/query-e76119227a3fcb00348279467161b081128d2a2cf67057da0e4b726463ceb40a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n d.id,\n d.username,\n d.display_name,\n d.verified,\n d.admin,\n d.github_user_id as github_id\n FROM developers d\n INNER JOIN auth_tokens a ON d.id = a.developer_id\n WHERE a.token = $1", + "query": "SELECT\n d.id,\n d.username,\n d.display_name,\n d.verified,\n d.admin,\n d.github_user_id as github_id\n FROM developers d\n INNER JOIN auth_tokens a ON d.id = a.developer_id\n WHERE a.token = $1\n AND (\n expires_at IS NULL\n OR expires_at > NOW()\n )", "describe": { "columns": [ { @@ -48,5 +48,5 @@ false ] }, - "hash": "6e713aab78ba4c3c8c7f6ffcb35290f10c23e7f46964dcf50fc9305711e59632" + "hash": "e76119227a3fcb00348279467161b081128d2a2cf67057da0e4b726463ceb40a" } diff --git a/.sqlx/query-a984cc43636e01cb9c540b289bfb2597b0ae49034429ac12499b823e215a5867.json b/.sqlx/query-e7684a38fd3927790617d3b247da3f3d4d4fdcde33256b7a681285f63240d2ad.json similarity index 88% rename from .sqlx/query-a984cc43636e01cb9c540b289bfb2597b0ae49034429ac12499b823e215a5867.json rename to .sqlx/query-e7684a38fd3927790617d3b247da3f3d4d4fdcde33256b7a681285f63240d2ad.json index 5213671..b861a2e 100644 --- a/.sqlx/query-a984cc43636e01cb9c540b289bfb2597b0ae49034429ac12499b823e215a5867.json +++ b/.sqlx/query-e7684a38fd3927790617d3b247da3f3d4d4fdcde33256b7a681285f63240d2ad.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO geode_versions\n\t\t\t\t(tag, prerelease, mac, win, android, ios, commit_hash)\n\t\t\tVALUES\n\t\t\t\t($1, $2, $3, $4, $5, $6, $7)", + "query": "INSERT INTO geode_versions\n (tag, prerelease, mac, win, android, ios, commit_hash)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7)", "describe": { "columns": [], "parameters": { @@ -92,5 +92,5 @@ }, "nullable": [] }, - "hash": "a984cc43636e01cb9c540b289bfb2597b0ae49034429ac12499b823e215a5867" + "hash": "e7684a38fd3927790617d3b247da3f3d4d4fdcde33256b7a681285f63240d2ad" } diff --git a/.sqlx/query-e94cd14f1e45bee623f55247b3ea4a95130aba83e5f5a661c9b45b63277f3da2.json b/.sqlx/query-e94cd14f1e45bee623f55247b3ea4a95130aba83e5f5a661c9b45b63277f3da2.json new file mode 100644 index 0000000..c04873f --- /dev/null +++ b/.sqlx/query-e94cd14f1e45bee623f55247b3ea4a95130aba83e5f5a661c9b45b63277f3da2.json @@ -0,0 +1,167 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n m.id, m.repository, m.about, m.changelog, m.featured, m.download_count as mod_download_count, m.created_at, m.updated_at,\n mv.id as version_id, mv.name, mv.description, mv.version, mv.download_link, mv.download_count as mod_version_download_count,\n mv.created_at as mod_version_created_at, mv.updated_at as mod_version_updated_at,\n mv.hash,\n format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as \"geode!: _\",\n mv.early_load, mv.api, mv.mod_id, mvs.status as \"status: _\", mvs.info\n FROM mods m\n INNER JOIN mod_versions mv ON m.id = mv.mod_id\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id\n WHERE m.id = $1\n AND ($2 = false OR mvs.status = 'accepted')\n ORDER BY mv.id DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "repository", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "about", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "changelog", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "featured", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "mod_download_count", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "version_id", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "description", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "download_link", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "mod_version_download_count", + "type_info": "Int4" + }, + { + "ordinal": 14, + "name": "mod_version_created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 15, + "name": "mod_version_updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 16, + "name": "hash", + "type_info": "Text" + }, + { + "ordinal": 17, + "name": "geode!: _", + "type_info": "Text" + }, + { + "ordinal": 18, + "name": "early_load", + "type_info": "Bool" + }, + { + "ordinal": 19, + "name": "api", + "type_info": "Bool" + }, + { + "ordinal": 20, + "name": "mod_id", + "type_info": "Text" + }, + { + "ordinal": 21, + "name": "status: _", + "type_info": { + "Custom": { + "name": "mod_version_status", + "kind": { + "Enum": [ + "pending", + "rejected", + "accepted", + "unlisted" + ] + } + } + } + }, + { + "ordinal": 22, + "name": "info", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Bool" + ] + }, + "nullable": [ + false, + true, + true, + true, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + null, + false, + false, + false, + false, + true + ] + }, + "hash": "e94cd14f1e45bee623f55247b3ea4a95130aba83e5f5a661c9b45b63277f3da2" +} diff --git a/.sqlx/query-ee8153251bce64dfd5ca3564b8ac6731b362d9c355a01b7b1e3de2bd60d51767.json b/.sqlx/query-ee8153251bce64dfd5ca3564b8ac6731b362d9c355a01b7b1e3de2bd60d51767.json new file mode 100644 index 0000000..fcb7e1d --- /dev/null +++ b/.sqlx/query-ee8153251bce64dfd5ca3564b8ac6731b362d9c355a01b7b1e3de2bd60d51767.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO github_loader_release_stats (total_download_count, latest_loader_version)\n VALUES ($1, $2)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [] + }, + "hash": "ee8153251bce64dfd5ca3564b8ac6731b362d9c355a01b7b1e3de2bd60d51767" +} diff --git a/.sqlx/query-9320782da17283c65ca2ef92bf4992d449bef38737eedd99d3038b13b7a40fb9.json b/.sqlx/query-f1945f11dce4a240cccbf4e8c2f282dfd1dc2cb69bb9803e0a7a4d648239389f.json similarity index 60% rename from .sqlx/query-9320782da17283c65ca2ef92bf4992d449bef38737eedd99d3038b13b7a40fb9.json rename to .sqlx/query-f1945f11dce4a240cccbf4e8c2f282dfd1dc2cb69bb9803e0a7a4d648239389f.json index 8eb68f7..30b2af7 100644 --- a/.sqlx/query-9320782da17283c65ca2ef92bf4992d449bef38737eedd99d3038b13b7a40fb9.json +++ b/.sqlx/query-f1945f11dce4a240cccbf4e8c2f282dfd1dc2cb69bb9803e0a7a4d648239389f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT s.checked_at, s.total_download_count\n FROM github_loader_release_stats s\n ORDER BY s.checked_at DESC\n ", + "query": "SELECT s.checked_at, s.total_download_count\n FROM github_loader_release_stats s\n ORDER BY s.checked_at DESC", "describe": { "columns": [ { @@ -22,5 +22,5 @@ false ] }, - "hash": "9320782da17283c65ca2ef92bf4992d449bef38737eedd99d3038b13b7a40fb9" + "hash": "f1945f11dce4a240cccbf4e8c2f282dfd1dc2cb69bb9803e0a7a4d648239389f" } diff --git a/Cargo.lock b/Cargo.lock index cefe22f..8a5c7a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1177,7 +1177,9 @@ dependencies = [ "serde_json", "sha256", "sqlx", + "thiserror 2.0.12", "tokio", + "url", "uuid", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index 7e0f462..da7a7be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ reqwest = { version = "0.12", default-features = false, features = [ "gzip", "rustls-tls", ] } +url = { version = "2" } uuid = { version = "1.8", features = ["v4", "fast-rng", "macro-diagnostics"] } zip = "4.2.0" sha256 = "1.5" @@ -42,3 +43,4 @@ log4rs = { version = "1.3.0", features = [ "threshold_filter", ] } ring = "0.17.12" +thiserror = "2.0.12" diff --git a/src/auth/github.rs b/src/auth/github.rs index 7f0e26c..e29f416 100644 --- a/src/auth/github.rs +++ b/src/auth/github.rs @@ -1,5 +1,5 @@ +use crate::auth::AuthenticationError; use crate::database::repository::github_login_attempts; -use crate::types::api::ApiError; use crate::types::models::github_login_attempt::StoredLoginAttempt; use reqwest::{header::HeaderValue, Client}; use serde::{Deserialize, Serialize}; @@ -16,6 +16,40 @@ pub struct GithubStartAuth { interval: i32, } +#[derive(Deserialize)] +pub enum GithubDeviceFlowErrorString { + #[serde(rename(deserialize = "authorization_pending"))] + AuthorizationPending, + #[serde(rename(deserialize = "slow_down"))] + SlowDown, + #[serde(rename(deserialize = "expired_token"))] + ExpiredToken, + #[serde(rename(deserialize = "unsupported_grant_type"))] + UnsupportedGrantType, + #[serde(rename(deserialize = "incorrect_client_credentials"))] + IncorrectClientCredentials, + #[serde(rename(deserialize = "incorrect_device_code"))] + IncorrectDeviceCode, + #[serde(rename(deserialize = "access_denied"))] + AccessDenied, + #[serde(rename(deserialize = "device_flow_disabled"))] + DeviceFlowDisabled, + Unknown, +} + +impl Default for GithubDeviceFlowErrorString { + fn default() -> Self { + GithubDeviceFlowErrorString::Unknown + } +} + +#[derive(Deserialize)] +pub struct GithubErrorResponse { + error: GithubDeviceFlowErrorString, + error_description: String, + error_uri: String, +} + pub struct GithubClient { client_id: String, client_secret: String, @@ -55,7 +89,7 @@ impl GithubClient { &self, ip: IpNetwork, pool: &mut PgConnection, - ) -> Result { + ) -> Result { if let Some(r) = github_login_attempts::get_one_by_ip(ip, pool).await? { if r.is_expired() { let uuid = Uuid::parse_str(&r.uuid).unwrap(); @@ -74,10 +108,7 @@ impl GithubClient { })) .send() .await - .map_err(|e| { - log::error!("Failed to start OAuth device flow with GitHub: {}", e); - ApiError::InternalError - })?; + .inspect_err(|e| log::error!("Failed to start OAuth device flow with GitHub: {e}"))?; if !res.status().is_success() { log::error!( @@ -85,16 +116,20 @@ impl GithubClient { res.status(), res.text().await.unwrap_or("No body received".into()) ); - return Err(ApiError::InternalError); + return Err(AuthenticationError::InternalError( + "Failed to start GitHub device flow".into(), + )); } - let body = res.json::().await.map_err(|e| { - log::error!( - "Failed to parse OAuth device flow response from GitHub: {}", - e - ); - ApiError::InternalError - })?; + let body = res + .json::() + .await + .inspect_err(|e| { + log::error!("Failed to parse OAuth device flow response from GitHub: {e}") + }) + .or(Err(AuthenticationError::InternalError( + "Failed to parse response from GitHub".into(), + )))?; github_login_attempts::create( ip, @@ -106,6 +141,7 @@ impl GithubClient { &mut *pool, ) .await + .map_err(|e| e.into()) } pub async fn poll_github( @@ -113,7 +149,7 @@ impl GithubClient { code: &str, is_device: bool, redirect_uri: Option<&str>, - ) -> Result { + ) -> Result { let json = { if is_device { json!({ @@ -146,99 +182,115 @@ impl GithubClient { .json(&json) .send() .await - .map_err(|e| { - log::error!("Failed to poll GitHub for developer access token: {}", e); - ApiError::InternalError + .inspect_err(|e| { + log::error!("Failed to poll GitHub for developer access token: {e}") })?; Ok(resp .json::() .await - .map_err(|e| { - log::error!("Failed to decode GitHub response: {}", e); - ApiError::InternalError - })? + .inspect_err(|e| log::error!("Failed to decode GitHub response: {e}"))? .get("access_token") - .ok_or(ApiError::BadRequest("Request not accepted by user".into()))? + .ok_or(AuthenticationError::UserAuthPending)? .as_str() .ok_or_else(|| { log::error!("Invalid access_token received from GitHub"); - ApiError::InternalError + AuthenticationError::InternalError( + "Failed to retrieve access token from GitHub".into(), + ) })? .to_string()) } - - pub async fn get_user(&self, token: &str) -> Result { + pub async fn get_user(&self, token: &str) -> Result { let resp = Client::new() .get("https://api.github.com/user") .header("Accept", HeaderValue::from_str("application/json").unwrap()) .header("User-Agent", "geode_index") .bearer_auth(token) .send() - .await - .map_err(|e| { - log::error!("Request to https://api.github.com/user failed: {}", e); - ApiError::InternalError - })?; + .await?; if !resp.status().is_success() { - return Err(ApiError::InternalError); + log::error!( + "github::get_user: received non-2xx response: {}. Body: {}", + resp.status(), + resp.text().await.unwrap_or("No response body".into()) + ); + return Err(AuthenticationError::InternalError( + "Failed to fetch user from GitHub API, received non 2xx response".into(), + )); } - resp.json::().await.map_err(|e| { - log::error!("Failed to create GitHubFetchedUser: {}", e); - ApiError::InternalError - }) + resp.json::() + .await + .inspect_err(|e| log::error!("github::get_user: failed to parse response: {e}")) + .or(Err(AuthenticationError::InternalError( + "Failed to parse user JSON received from GitHub".into(), + ))) } - pub async fn get_installation(&self, token: &str) -> Result { + pub async fn get_installation( + &self, + token: &str, + ) -> Result { let client = Client::new(); - let resp = match client + let resp = client .get("https://api.github.com/installation/repositories") .header("Accept", HeaderValue::from_str("application/json").unwrap()) .header("User-Agent", "geode_index") .bearer_auth(token) .send() .await - { - Err(e) => { - log::info!("{}", e); - return Err(ApiError::InternalError); - } - Ok(r) => r, - }; + .inspect_err(|e| { + log::error!("github::get_installation: failed to fetch repositories: {e}") + })?; if !resp.status().is_success() { - return Err(ApiError::InternalError); + log::error!( + "github::get_installation: received non-2xx response: {}. Body: {}", + resp.status(), + resp.text().await.unwrap_or("No response body".into()) + ); + return Err(AuthenticationError::InternalError( + "Received non-2xx response from GitHub".into(), + )); } - let body = match resp.json::().await { - Err(e) => { - log::error!("{}", e); - return Err(ApiError::InternalError); - } - Ok(b) => b, - }; + let body = resp + .json::() + .await + .inspect_err(|e| log::error!("github::get_installation: failed to parse response: {e}")) + .or(Err(AuthenticationError::InternalError( + "Failed to parse response from GitHub".into(), + )))?; - let repos = match body.get("repositories").and_then(|r| r.as_array()) { - None => { - return Err(ApiError::InternalError); - } - Some(r) => r, - }; + let repos = body.get("repositories").and_then(|r| r.as_array()).ok_or( + AuthenticationError::InternalError( + "Failed to get repository array from GitHub response".into(), + ), + )?; if repos.len() != 1 { - return Err(ApiError::InternalError); + return Err(AuthenticationError::InternalError( + "Failed to get repository from GitHub: array size isn't 1".into(), + )); } let owner = repos[0] .get("owner") - .ok_or(ApiError::InternalError)? + .ok_or(AuthenticationError::InternalError( + "Didn't find owner key on repository".into(), + ))? .clone(); - serde_json::from_value(owner).map_err(|e| { - log::error!("Failed to create GitHubFetchedUser: {}", e); - ApiError::InternalError - }) + serde_json::from_value(owner) + .inspect_err(|e| { + log::error!( + "github::get_installation: failed to extract owner from serde_json value: {e}" + ) + }) + .or(Err(AuthenticationError::InternalError( + "Failed to get GitHub user from installation".into(), + ))) } } diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 72246d3..9d56c68 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1 +1,19 @@ pub mod github; + +#[derive(thiserror::Error, Debug)] +pub enum AuthenticationError { + #[error("No authentication token provided")] + NoToken, + #[error("Provided token is invalid")] + InvalidToken, + #[error("User auth pending")] + UserAuthPending, + #[error("Failed to communicate with GitHub")] + RequestError(#[from] reqwest::Error), + #[error("{0}")] + InternalError(String), + #[error("{0}")] + Database(#[from] crate::database::DatabaseError), + #[error("Unknown database error")] + SqlxError(#[from] sqlx::Error), +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 4e69383..294e31f 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1 +1,9 @@ -pub mod repository; \ No newline at end of file +pub mod repository; + +#[derive(thiserror::Error, Debug)] +pub enum DatabaseError { + #[error("Invalid input: {0}")] + InvalidInput(String), + #[error("Unknown database error")] + SqlxError(#[from] sqlx::Error) +} diff --git a/src/database/repository/auth_tokens.rs b/src/database/repository/auth_tokens.rs index da07885..2709110 100644 --- a/src/database/repository/auth_tokens.rs +++ b/src/database/repository/auth_tokens.rs @@ -1,4 +1,4 @@ -use crate::types::api::ApiError; +use crate::database::DatabaseError; use chrono::{Days, Utc}; use sqlx::PgConnection; use uuid::Uuid; @@ -8,7 +8,7 @@ pub async fn generate_token( developer_id: i32, with_expiry: bool, conn: &mut PgConnection, -) -> Result { +) -> Result { let token = Uuid::new_v4(); let hash = sha256::digest(token.to_string()); let expiry = { @@ -28,19 +28,14 @@ pub async fn generate_token( ) .execute(&mut *conn) .await - .map_err(|e| { - log::error!( - "Failed to insert auth_token for developer {}: {}", - developer_id, - e - ); - ApiError::DbError + .inspect_err(|e| { + log::error!("Failed to insert auth_token for developer {developer_id}: {e}") })?; Ok(token) } -pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { let hash = sha256::digest(token.to_string()); sqlx::query!( @@ -50,10 +45,7 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Ap ) .execute(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to remove auth token: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to remove auth token: {e}"))?; Ok(()) } @@ -61,7 +53,7 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Ap pub async fn remove_developer_tokens( developer_id: i32, conn: &mut PgConnection, -) -> Result<(), ApiError> { +) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM auth_tokens WHERE developer_id = $1", @@ -69,25 +61,19 @@ pub async fn remove_developer_tokens( ) .execute(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to wipe developer tokens: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to wipe developer tokens: {e}"))?; Ok(()) } -pub async fn cleanup(conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM auth_tokens WHERE expires_at < NOW()" ) - .execute(conn) - .await - .map_err(|e| { - log::error!("Auth token cleanup failed: {}", e); - ApiError::DbError - })?; + .execute(conn) + .await + .inspect_err(|e| log::error!("Auth token cleanup failed: {e}"))?; Ok(()) } diff --git a/src/database/repository/dependencies.rs b/src/database/repository/dependencies.rs index 3bd6813..e784618 100644 --- a/src/database/repository/dependencies.rs +++ b/src/database/repository/dependencies.rs @@ -1,17 +1,21 @@ use sqlx::PgConnection; -use crate::types::{ - api::ApiError, - mod_json::ModJson, - models::dependency::{DependencyImportance, FetchedDependency, ModVersionCompare}, +use crate::{ + database::DatabaseError, + types::{ + mod_json::ModJson, + models::dependency::{DependencyImportance, FetchedDependency, ModVersionCompare}, + }, }; pub async fn create( mod_version_id: i32, json: &ModJson, conn: &mut PgConnection, -) -> Result, ApiError> { - let dependencies = json.prepare_dependencies_for_create()?; +) -> Result, DatabaseError> { + let dependencies = json.prepare_dependencies_for_create().map_err(|e| { + DatabaseError::InvalidInput(format!("Failed to parse dependencies from mod.json: {e}")) + })?; if dependencies.is_empty() { return Ok(vec![]); } @@ -55,11 +59,11 @@ pub async fn create( ) .fetch_all(conn) .await - .inspect_err(|e| log::error!("Failed to insert dependencies: {}", e)) - .or(Err(ApiError::DbError)) + .inspect_err(|e| log::error!("dependenceis::create query failed: {e}")) + .map_err(|e| e.into()) } -pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM dependencies WHERE dependent_id = $1", @@ -67,8 +71,7 @@ pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), ApiError> { ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to clear deps: {}", e)) - .or(Err(ApiError::DbError))?; - - Ok(()) + .inspect_err(|e| log::error!("dependencies::clear query failed: {e}")) + .map_err(|e| e.into()) + .map(|_| ()) } diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index 39b8887..426a2c4 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -1,26 +1,19 @@ -use crate::types::api::{ApiError, PaginatedData}; +use crate::database::DatabaseError; +use crate::types::api::PaginatedData; use crate::types::models::developer::{Developer, ModDeveloper}; use sqlx::PgConnection; use std::collections::HashMap; use uuid::Uuid; pub async fn index( - query: &str, + query: Option<&str>, page: i64, per_page: i64, conn: &mut PgConnection, -) -> Result, ApiError> { +) -> Result, DatabaseError> { let limit = per_page; let offset = (page - 1) * per_page; - let display_name_query = { - if query.is_empty() { - "".into() - } else { - format!("%{}%", query) - } - }; - let result = sqlx::query_as!( Developer, "SELECT @@ -32,23 +25,19 @@ pub async fn index( github_user_id as github_id FROM developers WHERE ( - ($1 = '' OR username = $1) - OR ($2 = '' OR display_name ILIKE $2) + ($1::text IS NULL OR username = $1) + OR ($1::text IS NULL OR display_name ILIKE '%' || $1 || '%') ) GROUP BY id - LIMIT $3 - OFFSET $4", + LIMIT $2 + OFFSET $3", query, - &display_name_query, limit, offset ) .fetch_all(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to fetch developers: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to fetch developers: {}", e))?; let count = index_count(query, &mut *conn).await?; @@ -58,40 +47,31 @@ pub async fn index( }) } -pub async fn index_count(query: &str, conn: &mut PgConnection) -> Result { - let display_name_query = { - if query.is_empty() { - "".into() - } else { - format!("%{}%", query) - } - }; - - Ok(sqlx::query!( +pub async fn index_count( + query: Option<&str>, + conn: &mut PgConnection, +) -> Result { + sqlx::query!( "SELECT COUNT(id) FROM developers WHERE ( - ($1 = '' OR username = $1) - OR ($2 = '' OR display_name ILIKE $2) + ($1::text IS NULL OR username = $1) + OR ($1::text IS NULL OR display_name ILIKE '%' || $1 || '%') )", - query, - display_name_query + query ) .fetch_one(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to fetch developer count: {}", e); - ApiError::DbError - })? - .count - .unwrap_or(0)) + .inspect_err(|e| log::error!("Failed to fetch developer count: {}", e)) + .map(|x| x.count.unwrap_or(0)) + .map_err(|e| e.into()) } pub async fn fetch_or_insert_github( github_id: i64, username: &str, conn: &mut PgConnection, -) -> Result { +) -> Result { match sqlx::query_as!( Developer, "SELECT @@ -107,10 +87,8 @@ pub async fn fetch_or_insert_github( ) .fetch_optional(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to fetch developer for GitHub id: {}", e); - ApiError::DbError - })? { + .inspect_err(|e| log::error!("Failed to fetch developer for GitHub id: {e}"))? + { Some(dev) => Ok(dev), None => Ok(insert_github(github_id, username, conn).await?), } @@ -120,7 +98,7 @@ async fn insert_github( github_id: i64, username: &str, conn: &mut PgConnection, -) -> Result { +) -> Result { sqlx::query_as!( Developer, "INSERT INTO developers(username, display_name, github_user_id) @@ -137,13 +115,11 @@ async fn insert_github( ) .fetch_one(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to insert developer: {}", e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("Failed to insert developer: {e}")) + .map_err(|e| e.into()) } -pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result, ApiError> { +pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result, DatabaseError> { sqlx::query_as!( Developer, "SELECT @@ -159,16 +135,14 @@ pub async fn get_one(id: i32, conn: &mut PgConnection) -> Result Result, ApiError> { +) -> Result, DatabaseError> { sqlx::query_as!( Developer, "SELECT @@ -184,16 +158,14 @@ pub async fn get_one_by_username( ) .fetch_optional(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to fetch developer {}: {}", username, e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("Failed to fetch developer {username}: {e}")) + .map_err(|x| x.into()) } pub async fn get_all_for_mod( mod_id: &str, conn: &mut PgConnection, -) -> Result, ApiError> { +) -> Result, DatabaseError> { sqlx::query_as!( ModDeveloper, "SELECT @@ -209,16 +181,14 @@ pub async fn get_all_for_mod( ) .fetch_all(conn) .await - .map_err(|e| { - log::error!("Failed to fetch developers for mod {}: {}", mod_id, e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("Failed to fetch developers for mod {}: {}", mod_id, e)) + .map_err(|e| e.into()) } pub async fn get_all_for_mods( mod_ids: &[String], conn: &mut PgConnection, -) -> Result>, ApiError> { +) -> Result>, DatabaseError> { if mod_ids.is_empty() { return Ok(HashMap::new()); } @@ -246,10 +216,7 @@ pub async fn get_all_for_mods( ) .fetch_all(conn) .await - .map_err(|e| { - log::error!("Failed to fetch developers for mods: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to fetch developers for mods: {}", e))?; let mut ret: HashMap> = HashMap::new(); @@ -271,8 +238,8 @@ pub async fn has_access_to_mod( dev_id: i32, mod_id: &str, conn: &mut PgConnection, -) -> Result { - Ok(sqlx::query!( +) -> Result { + sqlx::query!( "SELECT developer_id FROM mods_developers WHERE developer_id = $1 AND mod_id = $2", @@ -281,23 +248,23 @@ pub async fn has_access_to_mod( ) .fetch_optional(&mut *conn) .await - .map_err(|e| { + .inspect_err(|e| { log::error!( "Failed to find mod {} access for developer {}: {}", mod_id, dev_id, e ); - ApiError::DbError - })? - .is_some()) + }) + .map(|x| x.is_some()) + .map_err(|e| e.into()) } pub async fn owns_mod( dev_id: i32, mod_id: &str, conn: &mut PgConnection, -) -> Result { +) -> Result { Ok(sqlx::query!( "SELECT developer_id FROM mods_developers WHERE developer_id = $1 @@ -308,14 +275,13 @@ pub async fn owns_mod( ) .fetch_optional(&mut *conn) .await - .map_err(|e| { + .inspect_err(|e| { log::error!( "Failed to check mod {} owner for developer {}: {}", mod_id, dev_id, e - ); - ApiError::DbError + ) })? .is_some()) } @@ -323,7 +289,7 @@ pub async fn owns_mod( pub async fn get_owner_for_mod( mod_id: &str, conn: &mut PgConnection, -) -> Result { +) -> Result, DatabaseError> { sqlx::query_as!( Developer, "SELECT @@ -339,18 +305,10 @@ pub async fn get_owner_for_mod( AND md.is_owner = true", mod_id ) - .fetch_one(&mut *conn) + .fetch_optional(&mut *conn) .await - .map_err(|e| match e { - sqlx::Error::RowNotFound => { - log::error!("Mod {} doesn't have an owner!", mod_id); - ApiError::InternalError - } - _ => { - log::error!("Failed to fetch owner for mod {}", mod_id); - ApiError::InternalError - } - }) + .inspect_err(|e| log::error!("Failed to fetch owner for mod {mod_id}: {e}")) + .map_err(|e| e.into()) } pub async fn update_status( @@ -358,7 +316,7 @@ pub async fn update_status( verified: bool, admin: bool, conn: &mut PgConnection, -) -> Result { +) -> Result { sqlx::query_as!( Developer, "UPDATE developers @@ -378,17 +336,15 @@ pub async fn update_status( ) .fetch_one(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to update developer {}: {}", dev_id, e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("Failed to update developer {dev_id}: {e}")) + .map_err(|e| e.into()) } pub async fn update_profile( dev_id: i32, display_name: &str, conn: &mut PgConnection, -) -> Result { +) -> Result { sqlx::query_as!( Developer, "UPDATE developers @@ -406,16 +362,14 @@ pub async fn update_profile( ) .fetch_one(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to update profile for {}: {}", dev_id, e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("Failed to update profile for {dev_id}: {e}")) + .map_err(|e| e.into()) } pub async fn find_by_refresh_token( uuid: Uuid, conn: &mut PgConnection, -) -> Result, ApiError> { +) -> Result, DatabaseError> { let hash = sha256::digest(uuid.to_string()); sqlx::query_as!( Developer, @@ -434,8 +388,35 @@ pub async fn find_by_refresh_token( ) .fetch_optional(conn) .await - .map_err(|e| { - log::error!("Failed to search for developer by refresh token: {}", e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("Failed to search for developer by refresh token: {e}")) + .map_err(|e| e.into()) +} + +pub async fn find_by_token( + token: &Uuid, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + let hash = sha256::digest(token.to_string()); + sqlx::query_as!( + Developer, + "SELECT + d.id, + d.username, + d.display_name, + d.verified, + d.admin, + d.github_user_id as github_id + FROM developers d + INNER JOIN auth_tokens a ON d.id = a.developer_id + WHERE a.token = $1 + AND ( + expires_at IS NULL + OR expires_at > NOW() + )", + hash + ) + .fetch_optional(&mut *conn) + .await + .inspect_err(|e| log::error!("{}", e)) + .map_err(|e| e.into()) } diff --git a/src/database/repository/github_login_attempts.rs b/src/database/repository/github_login_attempts.rs index d924c3d..854760a 100644 --- a/src/database/repository/github_login_attempts.rs +++ b/src/database/repository/github_login_attempts.rs @@ -1,4 +1,4 @@ -use crate::types::api::ApiError; +use crate::database::DatabaseError; use crate::types::models::github_login_attempt::StoredLoginAttempt; use chrono::Utc; use sqlx::types::ipnetwork::IpNetwork; @@ -8,7 +8,7 @@ use uuid::Uuid; pub async fn get_one_by_ip( ip: IpNetwork, conn: &mut PgConnection, -) -> Result, ApiError> { +) -> Result, DatabaseError> { sqlx::query_as!( StoredLoginAttempt, "SELECT @@ -27,16 +27,14 @@ pub async fn get_one_by_ip( ) .fetch_optional(conn) .await - .map_err(|e| { - log::error!("Failed to fetch existing login attempt: {}", e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("Failed to fetch existing login attempt: {e}")) + .map_err(|e| e.into()) } pub async fn get_one_by_uuid( uuid: Uuid, pool: &mut PgConnection, -) -> Result, ApiError> { +) -> Result, DatabaseError> { sqlx::query_as!( StoredLoginAttempt, "SELECT @@ -55,10 +53,8 @@ pub async fn get_one_by_uuid( ) .fetch_optional(pool) .await - .map_err(|e| { - log::error!("Failed to fetch GitHub login attempt: {}", e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("Failed to fetch GitHub login attempt: {e}")) + .map_err(|e| e.into()) } pub async fn create( @@ -69,7 +65,7 @@ pub async fn create( uri: &str, user_code: &str, pool: &mut PgConnection, -) -> Result { +) -> Result { sqlx::query_as!( StoredLoginAttempt, "INSERT INTO github_login_attempts @@ -94,13 +90,11 @@ pub async fn create( ) .fetch_one(&mut *pool) .await - .map_err(|e| { - log::error!("Failed to insert new GitHub login attempt: {}", e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("Failed to insert new GitHub login attempt: {e}")) + .map_err(|e| e.into()) } -pub async fn poll_now(uuid: Uuid, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn poll_now(uuid: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { let now = Utc::now(); sqlx::query!( "UPDATE github_login_attempts @@ -111,22 +105,16 @@ pub async fn poll_now(uuid: Uuid, conn: &mut PgConnection) -> Result<(), ApiErro ) .execute(conn) .await - .map_err(|e| { - log::error!("Failed to poll GitHub login attempt: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to poll GitHub login attempt: {e}"))?; Ok(()) } -pub async fn remove(uuid: Uuid, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn remove(uuid: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!("DELETE FROM github_login_attempts WHERE uid = $1", uuid) .execute(conn) .await - .map_err(|e| { - log::error!("Failed to remove GitHub login attempt: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to remove GitHub login attempt: {e}"))?; Ok(()) } diff --git a/src/database/repository/github_web_logins.rs b/src/database/repository/github_web_logins.rs index 70b3568..cd8acf9 100644 --- a/src/database/repository/github_web_logins.rs +++ b/src/database/repository/github_web_logins.rs @@ -1,42 +1,32 @@ -use crate::types::api::ApiError; +use crate::database::DatabaseError; use sqlx::PgConnection; use uuid::Uuid; -pub async fn create_unique(conn: &mut PgConnection) -> Result { +pub async fn create_unique(conn: &mut PgConnection) -> Result { let unique = Uuid::new_v4(); sqlx::query!("INSERT INTO github_web_logins (state) VALUES ($1)", unique) .execute(conn) .await - .map_err(|e| { - log::error!("Failed to create GitHub web login secret: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to create GitHub web login secret: {e}"))?; Ok(unique) } -pub async fn exists(uuid: Uuid, conn: &mut PgConnection) -> Result { - Ok( - sqlx::query!("SELECT state FROM github_web_logins WHERE state = $1", uuid) - .fetch_optional(conn) - .await - .map_err(|e| { - log::error!("Failed to delete GitHub web login secret: {}", e); - ApiError::DbError - })? - .is_some(), - ) +pub async fn exists(uuid: Uuid, conn: &mut PgConnection) -> Result { + sqlx::query!("SELECT state FROM github_web_logins WHERE state = $1", uuid) + .fetch_optional(conn) + .await + .inspect_err(|e| log::error!("Failed to delete GitHub web login secret: {e}")) + .map(|x| x.is_some()) + .map_err(|e| e.into()) } -pub async fn remove(uuid: Uuid, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn remove(uuid: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!("DELETE FROM github_web_logins WHERE state = $1", uuid) .execute(conn) .await - .map_err(|e| { - log::error!("Failed to delete GitHub web login secret: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to delete GitHub web login secret: {e}"))?; Ok(()) } diff --git a/src/database/repository/incompatibilities.rs b/src/database/repository/incompatibilities.rs index 4844f68..8c43188 100644 --- a/src/database/repository/incompatibilities.rs +++ b/src/database/repository/incompatibilities.rs @@ -1,11 +1,13 @@ use sqlx::PgConnection; -use crate::types::{ - api::ApiError, - mod_json::ModJson, - models::{ - dependency::ModVersionCompare, - incompatibility::{FetchedIncompatibility, IncompatibilityImportance}, +use crate::{ + database::DatabaseError, + types::{ + mod_json::ModJson, + models::{ + dependency::ModVersionCompare, + incompatibility::{FetchedIncompatibility, IncompatibilityImportance}, + }, }, }; @@ -13,8 +15,12 @@ pub async fn create( mod_version_id: i32, json: &ModJson, conn: &mut PgConnection, -) -> Result, ApiError> { - let incompats = json.prepare_incompatibilities_for_create()?; +) -> Result, DatabaseError> { + let incompats = json.prepare_incompatibilities_for_create().map_err(|e| { + DatabaseError::InvalidInput(format!( + "Failed to parse incompatibilities from mod.json: {e}" + )) + })?; if incompats.is_empty() { return Ok(vec![]); } @@ -58,11 +64,11 @@ pub async fn create( ) .fetch_all(conn) .await - .inspect_err(|e| log::error!("Failed to insert dependencies: {}", e)) - .or(Err(ApiError::DbError)) + .inspect_err(|e| log::error!("incompatibilities::create query failed: {e}")) + .map_err(|e| e.into()) } -pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM incompatibilities WHERE mod_id = $1", @@ -70,8 +76,7 @@ pub async fn clear(id: i32, conn: &mut PgConnection) -> Result<(), ApiError> { ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to clear incompats: {}", e)) - .or(Err(ApiError::DbError))?; - - Ok(()) + .inspect_err(|e| log::error!("incompatibilities::clear query failed: {e}")) + .map_err(|e| e.into()) + .map(|_| ()) } diff --git a/src/database/repository/mod_downloads.rs b/src/database/repository/mod_downloads.rs index ee68180..4f64e15 100644 --- a/src/database/repository/mod_downloads.rs +++ b/src/database/repository/mod_downloads.rs @@ -1,4 +1,4 @@ -use crate::types::api::ApiError; +use crate::database::DatabaseError; use chrono::{Days, Utc}; use sqlx::types::ipnetwork::IpNetwork; use sqlx::PgConnection; @@ -7,7 +7,7 @@ pub async fn create( ip: IpNetwork, mod_version_id: i32, conn: &mut PgConnection, -) -> Result { +) -> Result { let result = sqlx::query!( "INSERT INTO mod_downloads (mod_version_id, ip) VALUES ($1, $2) @@ -17,13 +17,8 @@ pub async fn create( ) .execute(&mut *conn) .await - .map_err(|e| { - log::error!( - "Failed to insert new download for mod_version id {}: {}", - mod_version_id, - e - ); - ApiError::DbError + .inspect_err(|e| { + log::error!("Failed to insert new download for mod_version id {mod_version_id}: {e}"); })?; Ok(result.rows_affected() > 0) @@ -33,8 +28,8 @@ pub async fn has_downloaded_mod( ip: IpNetwork, mod_id: &str, conn: &mut PgConnection, -) -> Result { - Ok(sqlx::query!( +) -> Result { + sqlx::query!( "SELECT ip FROM mod_downloads md INNER JOIN mod_versions mv ON md.mod_version_id = mv.id WHERE mv.mod_id = $1 @@ -45,14 +40,12 @@ pub async fn has_downloaded_mod( ) .fetch_optional(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to check if mod has been downloaded: {}", e); - ApiError::DbError - })? - .is_some()) + .inspect_err(|e| log::error!("mod_downloads::has_downloaded_mod query error: {e}")) + .map_err(|e| e.into()) + .map(|x| x.is_some()) } -pub async fn cleanup(conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { let date = Utc::now().checked_sub_days(Days::new(30)).unwrap(); sqlx::query!( "DELETE FROM mod_downloads md @@ -61,10 +54,7 @@ pub async fn cleanup(conn: &mut PgConnection) -> Result<(), ApiError> { ) .execute(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to cleanup downloads: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("mod_downloads::cleanup query failed: {e}"))?; Ok(()) } diff --git a/src/database/repository/mod_gd_versions.rs b/src/database/repository/mod_gd_versions.rs index cc174dd..19efa36 100644 --- a/src/database/repository/mod_gd_versions.rs +++ b/src/database/repository/mod_gd_versions.rs @@ -1,16 +1,18 @@ use sqlx::PgConnection; -use crate::types::{ - api::ApiError, - mod_json::ModJson, - models::mod_gd_version::{DetailedGDVersion, GDVersionEnum, VerPlatform}, +use crate::{ + database::DatabaseError, + types::{ + mod_json::ModJson, + models::mod_gd_version::{DetailedGDVersion, GDVersionEnum, VerPlatform}, + }, }; pub async fn create( mod_version_id: i32, json: &ModJson, conn: &mut PgConnection, -) -> Result { +) -> Result { let create = json.gd.to_create_payload(json); let gd: Vec = create.iter().map(|x| x.gd).collect(); @@ -31,13 +33,12 @@ pub async fn create( ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to insert mod_gd_versions: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("mod_gd_versions::create query failed: {e}"))?; Ok(json.gd.clone()) } -pub async fn clear(mod_version_id: i32, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn clear(mod_version_id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM mod_gd_versions mgv WHERE mgv.mod_id = $1", @@ -45,8 +46,7 @@ pub async fn clear(mod_version_id: i32, conn: &mut PgConnection) -> Result<(), A ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to remove GD versions: {}", e)) - .or(Err(ApiError::DbError))?; - - Ok(()) + .inspect_err(|e| log::error!("incompatibilities::clear query failed: {e}")) + .map_err(|e| e.into()) + .map(|_| ()) } diff --git a/src/database/repository/mod_links.rs b/src/database/repository/mod_links.rs index f55b3e4..86547a5 100644 --- a/src/database/repository/mod_links.rs +++ b/src/database/repository/mod_links.rs @@ -1,6 +1,6 @@ use sqlx::PgConnection; -use crate::types::{api::ApiError, models::mod_link::ModLinks}; +use crate::{database::DatabaseError, types::models::mod_link::ModLinks}; pub async fn upsert( mod_id: &str, @@ -8,7 +8,7 @@ pub async fn upsert( homepage: Option, source: Option, conn: &mut PgConnection, -) -> Result { +) -> Result { sqlx::query!( "INSERT INTO mod_links (mod_id, community, homepage, source) @@ -26,8 +26,7 @@ pub async fn upsert( ) .execute(&mut *conn) .await - .inspect_err(|x| log::error!("Failed to upsert mod_links for id {}: {}", mod_id, x)) - .or(Err(ApiError::DbError))?; + .inspect_err(|x| log::error!("Failed to upsert mod_links for id {mod_id}: {x}"))?; Ok(ModLinks { mod_id: mod_id.into(), diff --git a/src/database/repository/mod_tags.rs b/src/database/repository/mod_tags.rs index 44ad647..7008801 100644 --- a/src/database/repository/mod_tags.rs +++ b/src/database/repository/mod_tags.rs @@ -1,8 +1,8 @@ -use crate::types::api::ApiError; +use crate::database::DatabaseError; use crate::types::models::tag::Tag; use sqlx::PgConnection; -pub async fn get_all_writable(conn: &mut PgConnection) -> Result, ApiError> { +pub async fn get_all_writable(conn: &mut PgConnection) -> Result, DatabaseError> { let tags = sqlx::query!( "SELECT id, @@ -14,10 +14,7 @@ pub async fn get_all_writable(conn: &mut PgConnection) -> Result, ApiEr ) .fetch_all(&mut *conn) .await - .map_err(|e| { - log::error!("mod_tags::get_all_writeable failed: {}", e); - ApiError::DbError - })? + .inspect_err(|e| log::error!("mod_tags::get_all_writeable failed: {e}"))? .into_iter() .map(|i| Tag { id: i.id, @@ -30,7 +27,10 @@ pub async fn get_all_writable(conn: &mut PgConnection) -> Result, ApiEr Ok(tags) } -pub async fn get_allowed_for_mod(id: &str, conn: &mut PgConnection) -> Result, ApiError> { +pub async fn get_allowed_for_mod( + id: &str, + conn: &mut PgConnection, +) -> Result, DatabaseError> { let mut writable = get_all_writable(&mut *conn).await?; let allowed_readonly = sqlx::query!( "SELECT DISTINCT @@ -46,8 +46,7 @@ pub async fn get_allowed_for_mod(id: &str, conn: &mut PgConnection) -> Result Result Result, ApiError> { +pub async fn get_all(conn: &mut PgConnection) -> Result, DatabaseError> { let tags = sqlx::query!( "SELECT id, @@ -73,14 +72,11 @@ pub async fn get_all(conn: &mut PgConnection) -> Result, ApiError> { ) .fetch_all(&mut *conn) .await - .map_err(|e| { - log::error!("mod_tags::get_all failed: {}", e); - ApiError::DbError - })? + .inspect_err(|e| log::error!("mod_tags::get_all failed: {e}"))? .into_iter() .map(|i| Tag { id: i.id, - display_name: i.display_name.unwrap_or_else(|| i.name.clone()), + display_name: i.display_name.unwrap_or(i.name.clone()), name: i.name, is_readonly: i.is_readonly, }) @@ -89,7 +85,7 @@ pub async fn get_all(conn: &mut PgConnection) -> Result, ApiError> { Ok(tags) } -pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, ApiError> { +pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, DatabaseError> { sqlx::query!( "SELECT id, @@ -103,10 +99,8 @@ pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, ) .fetch_all(&mut *conn) .await - .map_err(|e| { - log::error!("mod_tags::get_tags failed: {}", e); - ApiError::DbError - }) + .inspect_err(|e| log::error!("mod_tags::get_tags failed: {e}")) + .map_err(|e| e.into()) .map(|vec| { vec.into_iter() .map(|i| Tag { @@ -119,42 +113,11 @@ pub async fn get_for_mod(id: &str, conn: &mut PgConnection) -> Result, }) } -pub async fn parse_tag_list( - tags: &[String], - mod_id: &str, - conn: &mut PgConnection, -) -> Result, ApiError> { - if tags.is_empty() { - return Ok(vec![]); - } - - let db_tags = get_allowed_for_mod(mod_id, conn).await?; - - let mut ret = Vec::new(); - for tag in tags { - if let Some(t) = db_tags.iter().find(|t| t.name == *tag) { - ret.push(t.clone()); - } else { - return Err(ApiError::BadRequest(format!( - "Tag '{}' isn't allowed. Only the following are allowed: '{}'", - tag, - db_tags - .into_iter() - .map(|t| t.name) - .collect::>() - .join(", ") - ))); - } - } - - Ok(ret) -} - pub async fn update_for_mod( id: &str, tags: &[Tag], conn: &mut PgConnection, -) -> Result<(), ApiError> { +) -> Result<(), DatabaseError> { let existing = get_for_mod(id, &mut *conn).await?; let insertable = tags @@ -179,8 +142,7 @@ pub async fn update_for_mod( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to remove tags: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to remove tags: {e}"))?; } if !insertable.is_empty() { @@ -198,8 +160,7 @@ pub async fn update_for_mod( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to insert tags: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to insert tags: {e}"))?; } Ok(()) diff --git a/src/database/repository/mod_version_statuses.rs b/src/database/repository/mod_version_statuses.rs index 6777305..37c9def 100644 --- a/src/database/repository/mod_version_statuses.rs +++ b/src/database/repository/mod_version_statuses.rs @@ -1,13 +1,13 @@ +use crate::database::DatabaseError; +use crate::types::models::mod_version_status::ModVersionStatusEnum; use sqlx::PgConnection; -use crate::types::{api::ApiError, models::mod_version_status::ModVersionStatusEnum}; - pub async fn create( mod_version_id: i32, status: ModVersionStatusEnum, info: Option, conn: &mut PgConnection, -) -> Result { +) -> Result { sqlx::query!( "INSERT INTO mod_version_statuses (mod_version_id, status, info, admin_id) @@ -19,7 +19,7 @@ pub async fn create( ) .fetch_one(conn) .await - .inspect_err(|e| log::error!("Failed to create status: {}", e)) - .or(Err(ApiError::DbError)) + .inspect_err(|e| log::error!("Failed to create mod_version_status: {e}")) .map(|i| i.id) + .map_err(|e| e.into()) } diff --git a/src/database/repository/mod_versions.rs b/src/database/repository/mod_versions.rs index 4cc7c13..ec95dea 100644 --- a/src/database/repository/mod_versions.rs +++ b/src/database/repository/mod_versions.rs @@ -1,5 +1,6 @@ +use super::mod_version_statuses; +use crate::database::DatabaseError; use crate::types::{ - api::ApiError, mod_json::ModJson, models::{ developer::Developer, mod_version::ModVersion, mod_version_status::ModVersionStatusEnum, @@ -9,8 +10,6 @@ use chrono::{DateTime, SecondsFormat, Utc}; use semver::Version; use sqlx::PgConnection; -use super::mod_version_statuses; - #[derive(sqlx::FromRow)] struct ModVersionRow { id: i32, @@ -67,7 +66,7 @@ pub async fn get_by_version_str( mod_id: &str, version: &str, conn: &mut PgConnection, -) -> Result, ApiError> { +) -> Result, DatabaseError> { sqlx::query_as!( ModVersionRow, r#"SELECT @@ -84,18 +83,18 @@ pub async fn get_by_version_str( mod_id, version ) - .fetch_optional(conn) - .await - .inspect_err(|e| log::error!("{}", e)) - .or(Err(ApiError::DbError)) - .map(|opt| opt.map(|x| x.into_mod_version())) + .fetch_optional(conn) + .await + .inspect_err(|e| log::error!("Failed to get mod_version by version string: {e}")) + .map_err(|e| e.into()) + .map(|opt| opt.map(|x| x.into_mod_version())) } pub async fn get_for_mod( mod_id: &str, statuses: Option<&[ModVersionStatusEnum]>, conn: &mut PgConnection, -) -> Result, ApiError> { +) -> Result, DatabaseError> { sqlx::query_as!( ModVersionRow, r#"SELECT @@ -113,14 +112,14 @@ pub async fn get_for_mod( mod_id, statuses as Option<&[ModVersionStatusEnum]> ) - .fetch_all(conn) - .await - .inspect_err(|e| log::error!("{}", e)) - .or(Err(ApiError::DbError)) - .map(|opt: Vec| opt.into_iter().map(|x| x.into_mod_version()).collect()) + .fetch_all(conn) + .await + .inspect_err(|e| log::error!("Failed to get mod_versions for mod {mod_id}: {e}")) + .map_err(|e| e.into()) + .map(|opt: Vec| opt.into_iter().map(|x| x.into_mod_version()).collect()) } -pub async fn increment_downloads(id: i32, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn increment_downloads(id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "UPDATE mod_versions SET download_count = download_count + 1 @@ -129,14 +128,7 @@ pub async fn increment_downloads(id: i32, conn: &mut PgConnection) -> Result<(), ) .execute(&mut *conn) .await - .map_err(|e| { - log::error!( - "Failed to increment downloads for mod_version {}: {}", - id, - e - ); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to increment downloads for mod_version {id}: {e}"))?; Ok(()) } @@ -145,15 +137,14 @@ pub async fn create_from_json( json: &ModJson, make_accepted: bool, conn: &mut PgConnection, -) -> Result { +) -> Result { sqlx::query!("SET CONSTRAINTS mod_versions_status_id_fkey DEFERRED") .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to update constraint: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to update constraint: {e}"))?; - let geode = Version::parse(&json.geode).or(Err(ApiError::BadRequest( - "Invalid geode version in mod.json".into(), + let geode = Version::parse(&json.geode).or(Err(DatabaseError::InvalidInput( + "mod.json geode version is invalid semver".into(), )))?; let meta = if geode.pre.is_empty() { @@ -190,8 +181,7 @@ pub async fn create_from_json( ) .fetch_one(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to insert mod_version: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to insert mod_version: {e}"))?; let id = row.id; @@ -208,14 +198,12 @@ pub async fn create_from_json( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to set status: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to set status: {e}"))?; sqlx::query!("SET CONSTRAINTS mod_versions_status_id_fkey IMMEDIATE") .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to update constraint: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to update constraint: {e}"))?; Ok(ModVersion { id, @@ -251,9 +239,9 @@ pub async fn update_pending_version( json: &ModJson, make_accepted: bool, conn: &mut PgConnection, -) -> Result { - let geode = Version::parse(&json.geode).or(Err(ApiError::BadRequest( - "Invalid geode version in mod.json".into(), +) -> Result { + let geode = Version::parse(&json.geode).or(Err(DatabaseError::InvalidInput( + "mod.json geode version is invalid semver".into(), )))?; let meta = if geode.pre.is_empty() { @@ -308,14 +296,12 @@ pub async fn update_pending_version( ) .fetch_one(&mut *conn) .await - .map_err(|err| { + .inspect_err(|err| { log::error!( - "Failed to update pending version {}-{}: {}", + "Failed to update pending version {}-{}: {err}", json.id, - json.version, - err - ); - ApiError::DbError + json.version + ) })?; if make_accepted { @@ -327,8 +313,7 @@ pub async fn update_pending_version( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to update tag for mod: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to update tag for mod: {e}"))?; } Ok(ModVersion { @@ -369,7 +354,7 @@ pub async fn update_version_status( info: Option<&str>, updated_by: &Developer, conn: &mut PgConnection, -) -> Result { +) -> Result { if version.status == status { return Ok(version); } @@ -388,8 +373,7 @@ pub async fn update_version_status( ) .execute(&mut *conn) .await - .inspect_err(|e| log::error!("{}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to update mod_version_status: {e}"))?; version.status = status; diff --git a/src/database/repository/mods.rs b/src/database/repository/mods.rs index e65d111..d6a15cb 100644 --- a/src/database/repository/mods.rs +++ b/src/database/repository/mods.rs @@ -1,4 +1,7 @@ -use crate::types::{api::ApiError, mod_json::ModJson, models::mod_entity::Mod}; +use crate::{ + database::DatabaseError, + types::{mod_json::ModJson, models::mod_entity::Mod}, +}; use chrono::{DateTime, SecondsFormat, Utc}; use sqlx::PgConnection; @@ -42,7 +45,7 @@ pub async fn get_one( id: &str, include_md: bool, conn: &mut PgConnection, -) -> Result, ApiError> { +) -> Result, DatabaseError> { if include_md { sqlx::query_as!( ModRecordGetOne, @@ -55,8 +58,8 @@ pub async fn get_one( ) .fetch_optional(conn) .await - .inspect_err(|e| log::error!("Failed to fetch mod {}: {}", id, e)) - .or(Err(ApiError::DbError)) + .inspect_err(|e| log::error!("Failed to fetch mod {id}: {e}")) + .map_err(|e| e.into()) .map(|x| x.map(|x| x.into_mod())) } else { sqlx::query_as!( @@ -71,13 +74,13 @@ pub async fn get_one( .fetch_optional(conn) .await .inspect_err(|e| log::error!("Failed to fetch mod {}: {}", id, e)) - .or(Err(ApiError::DbError)) + .map_err(|e| e.into()) .map(|x| x.map(|x| x.into_mod())) } } /// Does NOT check if the target mod exists -pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result { +pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result { sqlx::query_as!( ModRecordGetOne, "INSERT INTO mods ( @@ -100,8 +103,8 @@ pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result Result<(), ApiError> { +) -> Result<(), DatabaseError> { assign_developer(id, developer_id, true, conn).await } @@ -118,7 +121,7 @@ pub async fn assign_developer( developer_id: i32, owner: bool, conn: &mut PgConnection, -) -> Result<(), ApiError> { +) -> Result<(), DatabaseError> { sqlx::query!( "INSERT INTO mods_developers (mod_id, developer_id, is_owner) VALUES ($1, $2, $3)", @@ -129,24 +132,17 @@ pub async fn assign_developer( .execute(conn) .await .inspect_err(|x| { - log::error!( - "Couldn't assign developer {} on mod {} (owner {}): {}", - developer_id, - id, - owner, - x - ) + log::error!("Couldn't assign developer {developer_id} on mod {id} (owner {owner}): {x}") }) - .or(Err(ApiError::DbError))?; - - Ok(()) + .map(|_| ()) + .map_err(|e| e.into()) } pub async fn unassign_developer( id: &str, developer_id: i32, conn: &mut PgConnection, -) -> Result<(), ApiError> { +) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM mods_developers WHERE mod_id = $1 @@ -164,35 +160,28 @@ pub async fn unassign_developer( x ) }) - .or(Err(ApiError::DbError))?; - - Ok(()) + .map(|_| ()) + .map_err(|e| e.into()) } -pub async fn is_featured(id: &str, conn: &mut PgConnection) -> Result { +pub async fn is_featured(id: &str, conn: &mut PgConnection) -> Result { Ok(sqlx::query!("SELECT featured FROM mods WHERE id = $1", id) .fetch_optional(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to check if mod {} exists: {}", id, e); - ApiError::DbError - })? + .inspect_err(|e| log::error!("Failed to check if mod {id} is featured: {e}"))? .map(|row| row.featured) .unwrap_or(false)) } -pub async fn exists(id: &str, conn: &mut PgConnection) -> Result { +pub async fn exists(id: &str, conn: &mut PgConnection) -> Result { Ok(sqlx::query!("SELECT id FROM mods WHERE id = $1", id) .fetch_optional(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to check if mod {} exists: {}", id, e); - ApiError::DbError - })? + .inspect_err(|e| log::error!("Failed to check if mod {} exists: {}", id, e))? .is_some()) } -pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result>, ApiError> { +pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result>, DatabaseError> { struct QueryResult { image: Option>, } @@ -209,10 +198,7 @@ pub async fn get_logo(id: &str, conn: &mut PgConnection) -> Result Result Result<(), ApiError> { +pub async fn increment_downloads(id: &str, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "UPDATE mods SET download_count = download_count + 1 @@ -232,10 +218,7 @@ pub async fn increment_downloads(id: &str, conn: &mut PgConnection) -> Result<() ) .execute(&mut *conn) .await - .map_err(|e| { - log::error!("Failed to increment downloads for mod {}: {}", id, e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to increment downloads for mod {id}: {e}"))?; Ok(()) } @@ -244,7 +227,7 @@ pub async fn update_with_json( mut the_mod: Mod, json: &ModJson, conn: &mut PgConnection, -) -> Result { +) -> Result { sqlx::query!( "UPDATE mods SET repository = $1, @@ -259,8 +242,7 @@ pub async fn update_with_json( ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to update mod: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to update mod: {}", e))?; the_mod.repository = json.repository.clone(); the_mod.about = json.about.clone(); @@ -273,7 +255,7 @@ pub async fn update_with_json_moved( mut the_mod: Mod, json: ModJson, conn: &mut PgConnection, -) -> Result { +) -> Result { sqlx::query!( "UPDATE mods SET repository = $1, @@ -290,8 +272,7 @@ pub async fn update_with_json_moved( ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to update mod: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to update mod: {e}"))?; the_mod.repository = json.repository; the_mod.about = json.about; @@ -302,7 +283,7 @@ pub async fn update_with_json_moved( /// Used when first version goes from pending to accepted. /// Makes it so versions that stay a lot in pending appear at the top of the newly created lists -pub async fn touch_created_at(id: &str, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn touch_created_at(id: &str, conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "UPDATE mods SET created_at = NOW() @@ -311,8 +292,7 @@ pub async fn touch_created_at(id: &str, conn: &mut PgConnection) -> Result<(), A ) .execute(conn) .await - .inspect_err(|e| log::error!("Failed to touch created_at for mod {}: {}", id, e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to touch created_at for mod {id}: {e}"))?; Ok(()) -} \ No newline at end of file +} diff --git a/src/database/repository/refresh_tokens.rs b/src/database/repository/refresh_tokens.rs index bd0c5d7..2d64318 100644 --- a/src/database/repository/refresh_tokens.rs +++ b/src/database/repository/refresh_tokens.rs @@ -1,9 +1,12 @@ -use crate::types::api::ApiError; +use crate::database::DatabaseError; use chrono::{Days, Utc}; use sqlx::PgConnection; use uuid::Uuid; -pub async fn generate_token(developer_id: i32, conn: &mut PgConnection) -> Result { +pub async fn generate_token( + developer_id: i32, + conn: &mut PgConnection, +) -> Result { let token = Uuid::new_v4(); let hash = sha256::digest(token.to_string()); let expiry = Utc::now().checked_add_days(Days::new(30)).unwrap(); @@ -17,15 +20,12 @@ pub async fn generate_token(developer_id: i32, conn: &mut PgConnection) -> Resul ) .execute(conn) .await - .map_err(|e| { - log::error!("Failed to insert refresh token: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to insert refresh token: {e}"))?; Ok(token) } -pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), DatabaseError> { let hash = sha256::digest(token.to_string()); sqlx::query!( "DELETE FROM refresh_tokens @@ -34,10 +34,7 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Ap ) .execute(conn) .await - .map_err(|e| { - log::error!("Failed to remove refresh token: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to remove refresh token: {e}"))?; Ok(()) } @@ -45,7 +42,7 @@ pub async fn remove_token(token: Uuid, conn: &mut PgConnection) -> Result<(), Ap pub async fn remove_developer_tokens( developer_id: i32, conn: &mut PgConnection, -) -> Result<(), ApiError> { +) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM refresh_tokens WHERE developer_id = $1", @@ -53,25 +50,19 @@ pub async fn remove_developer_tokens( ) .execute(conn) .await - .map_err(|e| { - log::error!("Failed to remove refresh tokens: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Failed to remove refresh tokens: {e}"))?; Ok(()) } -pub async fn cleanup(conn: &mut PgConnection) -> Result<(), ApiError> { +pub async fn cleanup(conn: &mut PgConnection) -> Result<(), DatabaseError> { sqlx::query!( "DELETE FROM refresh_tokens WHERE expires_at < NOW()" ) .execute(conn) .await - .map_err(|e| { - log::error!("Refresh token cleanup failed: {}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("Refresh token cleanup failed: {e}"))?; Ok(()) } diff --git a/src/endpoints/auth/github.rs b/src/endpoints/auth/github.rs index 82d9435..ae4c75e 100644 --- a/src/endpoints/auth/github.rs +++ b/src/endpoints/auth/github.rs @@ -9,10 +9,8 @@ use crate::database::repository::{ auth_tokens, developers, github_login_attempts, github_web_logins, refresh_tokens, }; use crate::endpoints::auth::TokensResponse; -use crate::{ - auth::github, - types::api::{ApiError, ApiResponse}, -}; +use crate::endpoints::ApiError; +use crate::{auth::github, types::api::ApiResponse}; #[derive(Deserialize)] struct PollParams { @@ -36,20 +34,21 @@ pub async fn start_github_login( data: web::Data, info: ConnectionInfo, ) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let client = github::GithubClient::new( data.github().client_id().to_string(), data.github().client_secret().to_string(), ); - let ip = match info.realip_remote_addr() { - None => return Err(ApiError::InternalError), - Some(i) => i, + + let Some(ip) = info.realip_remote_addr() else { + return Err(ApiError::InternalError( + "Couldn't get IP from request".into(), + )); }; - let net: IpNetwork = ip.parse().or(Err(ApiError::InternalError))?; + + let net: IpNetwork = ip.parse().or(Err(ApiError::InternalError( + "Couldn't get IP from request".into(), + )))?; let result = client.start_polling_auth(net, &mut pool).await?; Ok(web::Json(ApiResponse { @@ -60,11 +59,7 @@ pub async fn start_github_login( #[post("v1/login/github/web")] pub async fn start_github_web_login(data: web::Data) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let secret = github_web_logins::create_unique(&mut pool).await?; @@ -84,11 +79,7 @@ pub async fn github_web_callback( json: web::Json, data: web::Data, ) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let parsed = Uuid::parse_str(&json.state).or(Err(ApiError::BadRequest("Invalid secret".into())))?; @@ -108,19 +99,16 @@ pub async fn github_web_callback( .poll_github(&json.code, false, Some(data.front_url())) .await?; - let user = client - .get_user(&token) - .await - .map_err(|_| ApiError::InternalError)?; + let user = client.get_user(&token).await?; - let mut tx = pool.begin().await.or(Err(ApiError::TransactionError))?; + let mut tx = pool.begin().await?; let developer = developers::fetch_or_insert_github(user.id, &user.username, &mut tx).await?; let token = auth_tokens::generate_token(developer.id, true, &mut tx).await?; let refresh = refresh_tokens::generate_token(developer.id, &mut tx).await?; - tx.commit().await.or(Err(ApiError::TransactionError))?; + tx.commit().await?; Ok(web::Json(ApiResponse { error: "".to_string(), @@ -137,19 +125,13 @@ pub async fn poll_github_login( data: web::Data, connection_info: ConnectionInfo, ) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let uuid = Uuid::parse_str(&json.uuid).or(Err(ApiError::BadRequest("Invalid uuid".into())))?; let attempt = github_login_attempts::get_one_by_uuid(uuid, &mut pool) .await? - .ok_or(ApiError::BadRequest( - "No login attempt has been made for this UUID".into(), - ))?; + .ok_or(ApiError::NotFound("Couldn't find login attempt".into()))?; let net: IpNetwork = connection_info .realip_remote_addr() @@ -176,7 +158,7 @@ pub async fn poll_github_login( return Err(ApiError::BadRequest("Login attempt expired".to_string())); } - let mut tx = pool.begin().await.or(Err(ApiError::TransactionError))?; + let mut tx = pool.begin().await?; let client = github::GithubClient::new( data.github().client_id().to_string(), @@ -189,14 +171,15 @@ pub async fn poll_github_login( // Create a new transaction after this point, because we need to commit the removal of the login attempt // It would be invalid for GitHub anyway - tx.commit().await.or(Err(ApiError::TransactionError))?; + tx.commit().await?; let user = client .get_user(&token) .await - .map_err(|_| ApiError::InternalError)?; + .inspect_err(|e| log::error!("Failed to fetch user from GitHub: {e}")) + .map_err(|_| ApiError::InternalError("Failed to fetch user data from GitHub".into()))?; - let mut tx = pool.begin().await.or(Err(ApiError::TransactionError))?; + let mut tx = pool.begin().await?; let developer = developers::fetch_or_insert_github(user.id, &user.username, &mut tx).await?; @@ -211,7 +194,7 @@ pub async fn poll_github_login( } }; - tx.commit().await.or(Err(ApiError::TransactionError))?; + tx.commit().await?; if expiry { Ok(HttpResponse::build(StatusCode::OK).json(ApiResponse { @@ -248,17 +231,13 @@ pub async fn github_token_login( Ok(u) => u, }; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; - let mut tx = pool.begin().await.or(Err(ApiError::TransactionError))?; + let mut pool = data.db().acquire().await?; + let mut tx = pool.begin().await?; let developer = developers::fetch_or_insert_github(user.id, &user.username, &mut tx).await?; let token = auth_tokens::generate_token(developer.id, true, &mut tx).await?; - tx.commit().await.or(Err(ApiError::TransactionError))?; + tx.commit().await?; Ok(web::Json(ApiResponse { error: "".to_string(), diff --git a/src/endpoints/auth/mod.rs b/src/endpoints/auth/mod.rs index fe2d902..c005f06 100644 --- a/src/endpoints/auth/mod.rs +++ b/src/endpoints/auth/mod.rs @@ -1,7 +1,8 @@ use crate::config::AppData; use crate::database::repository::{auth_tokens, developers, refresh_tokens}; +use crate::endpoints::ApiError; use crate::extractors::auth::Auth; -use crate::types::api::{ApiError, ApiResponse}; +use crate::types::api::ApiResponse; use actix_web::{post, web, Responder}; use serde::{Deserialize, Serialize}; use sqlx::Acquire; @@ -31,11 +32,7 @@ pub async fn refresh_token( let refresh_token = Uuid::parse_str(&json.refresh_token) .or(Err(ApiError::BadRequest("Invalid refresh token".into())))?; - let mut conn = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut conn = data.db().acquire().await?; let found = developers::find_by_refresh_token(refresh_token, &mut conn) .await? @@ -43,7 +40,7 @@ pub async fn refresh_token( "Invalid or expired refresh token".into(), ))?; - let mut tx = conn.begin().await.or(Err(ApiError::TransactionError))?; + let mut tx = conn.begin().await?; let new_auth = auth_tokens::generate_token(found.id, true, &mut tx).await?; let new_refresh = refresh_tokens::generate_token(found.id, &mut tx).await?; @@ -53,7 +50,7 @@ pub async fn refresh_token( } refresh_tokens::remove_token(refresh_token, &mut tx).await?; - tx.commit().await.or(Err(ApiError::TransactionError))?; + tx.commit().await?; Ok(web::Json(ApiResponse { error: "".into(), diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index dcc2ead..2b84a4f 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -1,12 +1,13 @@ use actix_web::{delete, get, post, put, web, HttpResponse, Responder}; use serde::{Deserialize, Serialize}; +use super::ApiError; use crate::config::AppData; use crate::database::repository::{auth_tokens, developers, mods, refresh_tokens}; use crate::{ extractors::auth::Auth, types::{ - api::{ApiError, ApiResponse}, + api::ApiResponse, models::{ developer::ModDeveloper, mod_entity::Mod, mod_version_status::ModVersionStatusEnum, }, @@ -71,20 +72,16 @@ pub async fn developer_index( data: web::Data, query: web::Query, ) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let page: i64 = query.page.unwrap_or(1).max(1); let per_page = query.per_page.unwrap_or(10).clamp(1, 100); - let query = query.query.clone().unwrap_or_default(); + let query = query.query.clone(); Ok(web::Json(ApiResponse { error: "".into(), - payload: developers::index(&query, page, per_page, &mut pool).await?, + payload: developers::index(query.as_deref(), page, per_page, &mut pool).await?, })) } @@ -96,17 +93,13 @@ pub async fn add_developer_to_mod( auth: Auth, ) -> Result { let dev = auth.developer()?; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; if !mods::exists(&path.id, &mut pool).await? { return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); } if !developers::owns_mod(dev.id, &path.id, &mut pool).await? { - return Err(ApiError::Forbidden); + return Err(ApiError::Authorization); } let target = developers::get_one_by_username(&json.username, &mut pool) @@ -128,18 +121,14 @@ pub async fn remove_dev_from_mod( auth: Auth, ) -> Result { let dev = auth.developer()?; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; if !mods::exists(&path.id, &mut pool).await? { return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); } if !developers::owns_mod(dev.id, &path.id, &mut pool).await? { - return Err(ApiError::Forbidden); + return Err(ApiError::Authorization); } let target = developers::get_one_by_username(&path.username, &mut pool) @@ -157,10 +146,10 @@ pub async fn remove_dev_from_mod( } if !developers::has_access_to_mod(target.id, &path.id, &mut pool).await? { - return Ok(HttpResponse::NotFound().json(ApiResponse { - error: format!("{} is not a developer for this mod", target.username), - payload: "", - })); + return Err(ApiError::NotFound(format!( + "{} is not a developer for this mod", + target.username + ))); } mods::unassign_developer(&path.id, target.id, &mut pool).await?; @@ -174,11 +163,7 @@ pub async fn delete_token( auth: Auth, ) -> Result { let token = auth.token()?; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; auth_tokens::remove_token(token, &mut pool).await?; @@ -191,11 +176,7 @@ pub async fn delete_tokens( auth: Auth, ) -> Result { let dev = auth.developer()?; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; auth_tokens::remove_developer_tokens(dev.id, &mut pool).await?; refresh_tokens::remove_developer_tokens(dev.id, &mut pool).await?; @@ -215,11 +196,7 @@ pub async fn update_profile( auth: Auth, ) -> Result { let dev = auth.developer()?; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; if !json .display_name @@ -233,7 +210,7 @@ pub async fn update_profile( if json.display_name.len() < 2 { return Err(ApiError::BadRequest( - "Display name must have > 1 character".into(), + "Display name must have more than 1 character".into(), )); } @@ -248,7 +225,7 @@ struct GetOwnModsQuery { #[serde(default = "default_own_mods_status")] status: ModVersionStatusEnum, #[serde(default)] - only_owner: bool + only_owner: bool, } pub fn default_own_mods_status() -> ModVersionStatusEnum { @@ -262,12 +239,9 @@ pub async fn get_own_mods( auth: Auth, ) -> Result { let dev = auth.developer()?; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; - let mods: Vec = Mod::get_all_for_dev(dev.id, query.status, query.only_owner, &mut pool).await?; + let mut pool = data.db().acquire().await?; + let mods: Vec = + Mod::get_all_for_dev(dev.id, query.status, query.only_owner, &mut pool).await?; Ok(HttpResponse::Ok().json(ApiResponse { error: "".to_string(), payload: mods, @@ -293,11 +267,7 @@ pub async fn get_developer( data: web::Data, path: web::Path, ) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let result = developers::get_one(path.id, &mut pool) .await? .ok_or(ApiError::NotFound("Developer not found".into()))?; @@ -316,7 +286,7 @@ pub async fn update_developer( payload: web::Json, ) -> Result { let dev = auth.developer()?; - auth.admin()?; + auth.check_admin()?; if payload.admin.is_none() && payload.verified.is_none() { return Err(ApiError::BadRequest( @@ -324,11 +294,7 @@ pub async fn update_developer( )); } - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; if payload.admin.is_some() && dev.id == path.id { return Err(ApiError::BadRequest( diff --git a/src/endpoints/health.rs b/src/endpoints/health.rs index 66cea28..e8ae1b1 100644 --- a/src/endpoints/health.rs +++ b/src/endpoints/health.rs @@ -1,9 +1,8 @@ -use crate::types::api::ApiError; -use actix_web::{get, Responder}; +use actix_web::get; #[get("/")] -pub async fn health() -> Result { - Ok(r#" +pub async fn health() -> &'static str { + r#" _____ _____ ( ___ )----------------------------------------------------------------------( ___ ) | | | | @@ -22,5 +21,5 @@ pub async fn health() -> Result { | | \____|\___|\___/ \__,_|\___| |_|_| |_|\__,_|\___/_/\_(_) | | |___| |___| (_____)----------------------------------------------------------------------(_____) -"#) +"# } diff --git a/src/endpoints/loader.rs b/src/endpoints/loader.rs index fbf380b..7ab00a0 100644 --- a/src/endpoints/loader.rs +++ b/src/endpoints/loader.rs @@ -1,149 +1,147 @@ -use std::str::FromStr; -use actix_web::{web, get, post, Responder, HttpResponse}; +use actix_web::{get, post, web, HttpResponse, Responder}; use serde::Deserialize; +use std::str::FromStr; use sqlx::Acquire; +use crate::endpoints::ApiError; use crate::{ - extractors::auth::Auth, - types::{ - api::{ApiError, ApiResponse}, - models::{ - gd_version_alias::GDVersionAlias, - loader_version::{ - GetVersionsQuery, LoaderVersion, LoaderVersionCreate - }, - mod_gd_version::{ - DetailedGDVersion, - GDVersionEnum, - VerPlatform, - } - } - }, - config::AppData, + config::AppData, + extractors::auth::Auth, + types::{ + api::ApiResponse, + models::{ + gd_version_alias::GDVersionAlias, + loader_version::{GetVersionsQuery, LoaderVersion, LoaderVersionCreate}, + mod_gd_version::{DetailedGDVersion, GDVersionEnum, VerPlatform}, + }, + }, }; #[derive(Deserialize)] struct GetOneQuery { - platform: Option, - gd: Option, - #[serde(default)] - prerelease: bool, + platform: Option, + gd: Option, + #[serde(default)] + prerelease: bool, } #[derive(Deserialize)] struct GetOnePath { - version: String, + version: String, } #[get("v1/loader/versions/{version}")] pub async fn get_one( - path: web::Path, - data: web::Data, - query: web::Query + path: web::Path, + data: web::Data, + query: web::Query, ) -> Result { - let mut pool = data.db().acquire().await.or(Err(ApiError::DbAcquireError))?; - - let version = if path.version == "latest" { - let gd = if let Some(i) = &query.gd { - if let Ok(g) = GDVersionEnum::from_str(i) { - Some(g) - } else { - let platform = query.platform - .ok_or_else(|| ApiError::BadRequest("Platform is required when a version alias is given".to_string()))?; - Some(GDVersionAlias::find(platform, i, &mut pool).await?) - } - } else { - None - }; - - LoaderVersion::get_latest(gd, query.platform, query.prerelease, &mut pool).await? - } else { - LoaderVersion::get_one(&path.version, &mut pool).await? - }; - - Ok(web::Json(ApiResponse { - error: "".to_string(), - payload: version, - })) + let mut pool = data.db().acquire().await?; + + let version = if path.version == "latest" { + let gd = if let Some(i) = &query.gd { + if let Ok(g) = GDVersionEnum::from_str(i) { + Some(g) + } else { + let platform = query.platform.ok_or_else(|| { + ApiError::BadRequest( + "Platform is required when a version alias is given".into(), + ) + })?; + GDVersionAlias::find(platform, i, &mut pool).await? + } + } else { + None + }; + + LoaderVersion::get_latest(gd, query.platform, query.prerelease, &mut pool) + .await? + .ok_or(ApiError::NotFound("Latest version not found".into()))? + } else { + LoaderVersion::get_one(&path.version, &mut pool) + .await? + .ok_or(ApiError::NotFound("Not found".into()))? + }; + + Ok(web::Json(ApiResponse { + error: "".to_string(), + payload: version, + })) } #[derive(Deserialize)] struct CreateVersionBody { - pub tag: String, - #[serde(default)] - pub prerelease: bool, - pub commit_hash: String, - pub gd: DetailedGDVersion, + pub tag: String, + #[serde(default)] + pub prerelease: bool, + pub commit_hash: String, + pub gd: DetailedGDVersion, } #[post("v1/loader/versions")] pub async fn create_version( - data: web::Data, - payload: web::Json, - auth: Auth, + data: web::Data, + payload: web::Json, + auth: Auth, ) -> Result { - let dev = auth.developer()?; - let mut pool = data.db().acquire().await.or(Err(ApiError::DbAcquireError))?; - - if !dev.admin { - return Err(ApiError::Forbidden); - } - - let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?; - if let Err(e) = LoaderVersion::create_version(LoaderVersionCreate { - tag: payload.tag.trim_start_matches('v').to_string(), - prerelease: payload.prerelease, - commit_hash: payload.commit_hash.clone(), - win: payload.gd.win, - mac: payload.gd.mac, - android: payload.gd.android, - ios: payload.gd.ios, - }, &mut transaction).await { - transaction - .rollback() - .await - .or(Err(ApiError::TransactionError))?; - return Err(e); - } - - transaction - .commit() - .await - .or(Err(ApiError::TransactionError))?; - - Ok(HttpResponse::NoContent()) + let dev = auth.developer()?; + let mut pool = data.db().acquire().await?; + + if !dev.admin { + return Err(ApiError::Authorization); + } + + let mut tx = pool.begin().await?; + LoaderVersion::create_version( + LoaderVersionCreate { + tag: payload.tag.trim_start_matches('v').to_string(), + prerelease: payload.prerelease, + commit_hash: payload.commit_hash.clone(), + win: payload.gd.win, + mac: payload.gd.mac, + android: payload.gd.android, + ios: payload.gd.ios, + }, + &mut tx, + ) + .await?; + + tx.commit().await?; + + Ok(HttpResponse::NoContent()) } #[derive(Deserialize)] struct GetManyQuery { - pub gd: Option, - pub platform: Option, - pub per_page: Option, - pub page: Option, - pub prerelease: Option + pub gd: Option, + pub platform: Option, + pub per_page: Option, + pub page: Option, + pub prerelease: Option, } #[get("v1/loader/versions")] pub async fn get_many( - data: web::Data, - query: web::Query, + data: web::Data, + query: web::Query, ) -> Result { - let mut pool = data.db().acquire().await.or(Err(ApiError::DbAcquireError))?; - - let versions = LoaderVersion::get_many( - GetVersionsQuery { - gd: query.gd, - platform: query.platform, - prerelease: query.prerelease.unwrap_or_default() - }, - query.per_page.unwrap_or(10), - query.page.unwrap_or(1), - &mut pool - ).await?; - - Ok(web::Json(ApiResponse { - error: "".to_string(), - payload: versions, - })) + let mut pool = data.db().acquire().await?; + + let versions = LoaderVersion::get_many( + GetVersionsQuery { + gd: query.gd, + platform: query.platform, + prerelease: query.prerelease.unwrap_or_default(), + }, + query.per_page.unwrap_or(10), + query.page.unwrap_or(1), + &mut pool, + ) + .await?; + + Ok(web::Json(ApiResponse { + error: "".to_string(), + payload: versions, + })) } diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 833ac50..c903706 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -1,8 +1,69 @@ +use crate::{ + mod_zip::ModZipError, + types::{api::ApiResponse, models::mod_gd_version::PlatformParseError}, +}; +use actix_web::{http::StatusCode, HttpResponse}; + pub mod auth; pub mod developers; +pub mod health; +pub mod loader; pub mod mod_versions; pub mod mods; -pub mod tags; pub mod stats; -pub mod loader; -pub mod health; +pub mod tags; + +#[derive(thiserror::Error, Debug)] +pub enum ApiError { + #[error("Authentication error: {0}")] + Authentication(#[from] crate::auth::AuthenticationError), + #[error("You do not have acces to this resource")] + Authorization, + #[error("{0}")] + Database(#[from] crate::database::DatabaseError), + #[error("{0}")] + ModZip(#[from] ModZipError), + #[error("Database error")] + SqlxError(#[from] sqlx::Error), + #[error("Failed to parse response data: {0}")] + Json(#[from] serde_json::Error), + #[error("{0}")] + BadRequest(String), + #[error("{0}")] + TooManyRequests(String), + #[error("Internal error: {0}")] + InternalError(String), + #[error("{0}")] + NotFound(String), + #[error("Error: {0}")] + PlatformParseError(#[from] PlatformParseError), + #[error("Unable to unzip archive")] + Zip(#[from] zip::result::ZipError), + #[error("Failed to contact external resource: {0}")] + Reqwest(#[from] reqwest::Error), +} + +impl ApiError { + pub fn as_response(&self) -> ApiResponse { + ApiResponse { + error: self.to_string(), + payload: "".into(), + } + } +} + +impl actix_web::ResponseError for ApiError { + fn status_code(&self) -> StatusCode { + match self { + ApiError::Authentication(..) => StatusCode::UNAUTHORIZED, + ApiError::Authorization => StatusCode::FORBIDDEN, + ApiError::Json(..) => StatusCode::BAD_REQUEST, + ApiError::TooManyRequests(..) => StatusCode::TOO_MANY_REQUESTS, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).json(self.as_response()) + } +} diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index fbb60da..cb1e076 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -9,15 +9,17 @@ use crate::database::repository::{ dependencies, developers, incompatibilities, mod_downloads, mod_gd_versions, mod_links, mod_tags, mod_versions, mods, }; +use crate::endpoints::ApiError; use crate::events::mod_created::{ NewModAcceptedEvent, NewModVersionAcceptedEvent, NewModVersionVerification, }; use crate::mod_zip::{self, download_mod}; +use crate::types::models; use crate::webhook::discord::DiscordWebhook; use crate::{ extractors::auth::Auth, types::{ - api::{ApiError, ApiResponse}, + api::ApiResponse, mod_json::{split_version_and_compare, ModJson}, models::{ mod_gd_version::{GDVersionEnum, VerPlatform}, @@ -80,7 +82,7 @@ pub async fn get_version_index( query: web::Query, auth: Auth, ) -> Result { - let platforms = VerPlatform::parse_query_string(&query.platforms.clone().unwrap_or_default()); + let platforms = VerPlatform::parse_query_string(&query.platforms.clone().unwrap_or_default())?; let compare = query.compare.as_ref().map(|c| split_version_and_compare(c)); if compare.is_some() && compare.as_ref().unwrap().is_err() { @@ -92,11 +94,7 @@ pub async fn get_version_index( let compare = compare.map(|x| x.unwrap()); - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let has_extended_permissions = match auth.developer() { Ok(dev) => dev.admin || developers::has_access_to_mod(dev.id, &path.id, &mut pool).await?, @@ -133,11 +131,7 @@ pub async fn get_one( query: web::Query, auth: Auth, ) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let has_extended_permissions = match auth.developer() { Ok(dev) => dev.admin || developers::has_access_to_mod(dev.id, &path.id, &mut pool).await?, @@ -155,13 +149,16 @@ pub async fn get_one( }; let platform_string = query.platforms.clone().unwrap_or_default(); - let platforms = VerPlatform::parse_query_string(&platform_string); + let platforms = VerPlatform::parse_query_string(&platform_string)?; ModVersion::get_latest_for_mod(&path.id, gd, platforms, query.major, &mut pool).await? } else { ModVersion::get_one(&path.id, &path.version, true, false, &mut pool).await? } - }; + } + .ok_or(ApiError::NotFound( + "Couldn't find valid mod version for given filters".into(), + ))?; version.modify_metadata(data.app_url(), has_extended_permissions); Ok(web::Json(ApiResponse { @@ -185,21 +182,20 @@ pub async fn download_version( query: web::Query, info: ConnectionInfo, ) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let mod_version = { if path.version == "latest" { let platform_str = query.platforms.clone().unwrap_or_default(); - let platforms = VerPlatform::parse_query_string(&platform_str); + let platforms = VerPlatform::parse_query_string(&platform_str)?; ModVersion::get_latest_for_mod(&path.id, query.gd, platforms, query.major, &mut pool) .await? } else { ModVersion::get_one(&path.id, &path.version, false, false, &mut pool).await? } - }; + } + .ok_or(ApiError::NotFound( + "Couldn't find valid mod version for given filters".into(), + ))?; let url = mod_version.download_link; if data.disable_downloads() || mod_version.status != ModVersionStatusEnum::Accepted { @@ -209,27 +205,31 @@ pub async fn download_version( .finish()); } - let ip = match info.realip_remote_addr() { - None => return Err(ApiError::InternalError), - Some(i) => i, + let Some(ip) = info.realip_remote_addr() else { + return Err(ApiError::InternalError( + "Couldn't detect IP address of client".into(), + )); }; - let net: IpNetwork = ip.parse().or(Err(ApiError::InternalError))?; - let mut tx = pool.begin().await.or(Err(ApiError::TransactionError))?; + let net = ip.parse::().ok(); + + if let Some(net) = net { + let mut tx = pool.begin().await?; - let downloaded_mod_previously = - mod_downloads::has_downloaded_mod(net, &mod_version.mod_id, &mut tx).await?; - let inserted = mod_downloads::create(net, mod_version.id, &mut tx).await?; + let downloaded_mod_previously = + mod_downloads::has_downloaded_mod(net, &mod_version.mod_id, &mut tx).await?; + let inserted = mod_downloads::create(net, mod_version.id, &mut tx).await?; - if inserted { - mod_versions::increment_downloads(mod_version.id, &mut tx).await?; + if inserted { + mod_versions::increment_downloads(mod_version.id, &mut tx).await?; - if !downloaded_mod_previously { - mods::increment_downloads(&mod_version.mod_id, &mut tx).await?; + if !downloaded_mod_previously { + mods::increment_downloads(&mod_version.mod_id, &mut tx).await?; + } } - } - let _ = tx.commit().await; + let _ = tx.commit().await; + } Ok(HttpResponse::Found() .append_header(("Location", url)) @@ -244,11 +244,7 @@ pub async fn create_version( auth: Auth, ) -> Result { let dev = auth.developer()?; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let id = path.into_inner(); @@ -257,7 +253,7 @@ pub async fn create_version( .ok_or(ApiError::NotFound(format!("Mod {} not found", &id)))?; if !(developers::has_access_to_mod(dev.id, &the_mod.id, &mut pool).await?) { - return Err(ApiError::Forbidden); + return Err(ApiError::Authorization); } let versions = mod_versions::get_for_mod( @@ -291,10 +287,8 @@ pub async fn create_version( .collect(); let bytes = download_mod(&download_link, data.max_download_mb()).await?; - let json = ModJson::from_zip(bytes, &download_link, make_accepted).map_err(|err| { - log::error!("Failed to parse mod.json: {}", err); - ApiError::FilesystemError - })?; + let json = ModJson::from_zip(bytes, &download_link, make_accepted) + .inspect_err(|e| log::error!("Failed to parse mod.json: {e}"))?; if json.id != the_mod.id { return Err(ApiError::BadRequest(format!( "Request id {} does not match mod.json id {}", @@ -304,7 +298,7 @@ pub async fn create_version( json.validate()?; - let mut tx = pool.begin().await.or(Err(ApiError::TransactionError))?; + let mut tx = pool.begin().await?; let mut version: ModVersion = if versions.is_empty() { mod_versions::create_from_json(&json, make_accepted, &mut tx).await? @@ -312,7 +306,10 @@ pub async fn create_version( let latest = versions.first().unwrap(); let latest_version = semver::Version::parse(&latest.version) .inspect_err(|e| log::error!("Failed to parse locally stored version: {}", e)) - .or(Err(ApiError::InternalError))?; + .or(Err(ApiError::InternalError(format!( + "Failed to parse semver for existing mod version: {}", + &latest.version + ))))?; let new_version = semver::Version::parse(json.version.trim_start_matches('v')).or(Err( ApiError::BadRequest(format!("Invalid mod.json version: {}", json.version)), ))?; @@ -371,7 +368,7 @@ pub async fn create_version( } if let Some(tags) = &json.tags { if !tags.is_empty() { - let tags = mod_tags::parse_tag_list(tags, &the_mod.id, &mut tx).await?; + let tags = models::tag::parse_tag_list(tags, &the_mod.id, &mut tx).await?; mod_tags::update_for_mod(&the_mod.id, &tags, &mut tx).await?; } } @@ -379,10 +376,12 @@ pub async fn create_version( mods::update_with_json_moved(the_mod, json, &mut tx).await?; } - tx.commit().await.or(Err(ApiError::TransactionError))?; + tx.commit().await?; if make_accepted { - let owner = developers::get_owner_for_mod(&version.mod_id, &mut pool).await?; + let owner = developers::get_owner_for_mod(&version.mod_id, &mut pool) + .await? + .ok_or(ApiError::BadRequest("Mod doesn't have an owner".into()))?; NewModVersionAcceptedEvent { id: version.mod_id.clone(), @@ -413,18 +412,14 @@ pub async fn update_version( ) -> Result { let dev = auth.developer()?; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let the_mod = mods::get_one(&path.id, false, &mut pool) .await? .ok_or(ApiError::NotFound(format!("Mod {} not found", path.id)))?; if !dev.admin { - return Err(ApiError::Forbidden); + return Err(ApiError::Authorization); } let version = mod_versions::get_by_version_str(&the_mod.id, &path.version, &mut pool) @@ -445,7 +440,7 @@ pub async fn update_version( } let approved_count = ModVersion::get_accepted_count(version.mod_id.as_str(), &mut pool).await?; - let mut tx = pool.begin().await.or(Err(ApiError::TransactionError))?; + let mut tx = pool.begin().await?; let old_status = version.status; let version = mod_versions::update_version_status( @@ -490,7 +485,7 @@ pub async fn update_version( // Update tags with data from mod.json let tags = if let Some(tags) = &json.tags { - mod_tags::parse_tag_list(tags, &the_mod.id, &mut tx).await? + models::tag::parse_tag_list(tags, &the_mod.id, &mut tx).await? } else { vec![] }; @@ -500,12 +495,16 @@ pub async fn update_version( mods::update_with_json_moved(the_mod, json, &mut tx).await?; } - tx.commit().await.or(Err(ApiError::TransactionError))?; + tx.commit().await?; if payload.status == ModVersionStatusEnum::Accepted { let is_update = approved_count > 0; - let owner = developers::get_owner_for_mod(&version.mod_id, &mut pool).await?; + let owner = developers::get_owner_for_mod(&version.mod_id, &mut pool) + .await? + .ok_or(ApiError::InternalError( + "Couldn't find owner for mod".into(), + ))?; if !is_update { NewModAcceptedEvent { diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index c896b5d..862bc95 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -7,11 +7,13 @@ use crate::database::repository::mod_links; use crate::database::repository::mod_tags; use crate::database::repository::mod_versions; use crate::database::repository::mods; +use crate::endpoints::ApiError; use crate::events::mod_feature::ModFeaturedEvent; use crate::extractors::auth::Auth; use crate::mod_zip; -use crate::types::api::{create_download_link, ApiError, ApiResponse}; +use crate::types::api::{create_download_link, ApiResponse}; use crate::types::mod_json::ModJson; +use crate::types::models; use crate::types::models::incompatibility::Incompatibility; use crate::types::models::mod_entity::{Mod, ModUpdate}; use crate::types::models::mod_gd_version::{GDVersionEnum, VerPlatform}; @@ -63,18 +65,11 @@ pub async fn index( query: web::Query, auth: Auth, ) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; if let Some(s) = query.status { if s == ModVersionStatusEnum::Rejected { - let dev = auth.developer()?; - if !dev.admin { - return Err(ApiError::Forbidden); - } + auth.check_admin()?; } } @@ -97,11 +92,7 @@ pub async fn get( auth: Auth, ) -> Result { let dev = auth.developer().ok(); - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let has_extended_permissions = match auth.developer() { Ok(dev) => dev.admin || developers::has_access_to_mod(dev.id, &id, &mut pool).await?, @@ -156,11 +147,7 @@ pub async fn create( auth: Auth, ) -> Result { let dev = auth.developer()?; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let bytes = mod_zip::download_mod(&payload.download_link, data.max_download_mb()).await?; let json = ModJson::from_zip(bytes, &payload.download_link, false)?; json.validate()?; @@ -169,7 +156,7 @@ pub async fn create( if let Some(m) = &existing { if !developers::has_access_to_mod(dev.id, &m.id, &mut pool).await? { - return Err(ApiError::Forbidden); + return Err(ApiError::Authorization); } let versions = mod_versions::get_for_mod(&m.id, None, &mut pool).await?; @@ -182,7 +169,7 @@ pub async fn create( } } - let mut tx = pool.begin().await.or(Err(ApiError::TransactionError))?; + let mut tx = pool.begin().await?; let mod_already_exists = existing.is_some(); @@ -198,7 +185,7 @@ pub async fn create( } if let Some(tags) = &json.tags { - let tag_list = mod_tags::parse_tag_list(tags, &the_mod.id, &mut tx).await?; + let tag_list = models::tag::parse_tag_list(tags, &the_mod.id, &mut tx).await?; mod_tags::update_for_mod(&the_mod.id, &tag_list, &mut tx).await?; } if let Some(l) = json.links.clone() { @@ -227,7 +214,7 @@ pub async fn create( the_mod.developers = developers::get_all_for_mod(&the_mod.id, &mut tx).await?; the_mod.versions.insert(0, version); - tx.commit().await.or(Err(ApiError::TransactionError))?; + tx.commit().await?; for i in &mut the_mod.versions { i.modify_metadata(data.app_url(), false); @@ -251,11 +238,7 @@ pub async fn get_mod_updates( data: web::Data, query: web::Query, ) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; if query.platform == VerPlatform::Android || query.platform == VerPlatform::Mac { return Err(ApiError::BadRequest("Invalid platform. Use android32 / android64 for android and mac-intel / mac-arm for mac".to_string())); @@ -267,15 +250,12 @@ pub async fn get_mod_updates( .map(String::from) .collect::>(); - let geode = match semver::Version::parse(&query.geode) { - Ok(g) => g, - Err(e) => { - log::error!("{}", e); - return Err(ApiError::BadRequest( - "Invalid geode version format".to_string(), - )); - } - }; + let geode = semver::Version::parse(&query.geode).map_err(|_| { + ApiError::BadRequest(format!( + "Invalid mod.json geode version semver: {}", + query.geode + )) + })?; let mut result: Vec = Mod::get_updates(&ids, query.platform, &geode, query.gd, &mut pool).await?; @@ -320,11 +300,7 @@ pub async fn get_logo( path: web::Path, ) -> Result { use crate::database::repository::*; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let image: Option> = mods::get_logo(&path.into_inner(), &mut pool).await?; match image { @@ -352,45 +328,39 @@ pub async fn update_mod( auth: Auth, ) -> Result { let dev = auth.developer()?; - if !dev.admin { - return Err(ApiError::Forbidden); - } - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + auth.check_admin()?; + let mut pool = data.db().acquire().await?; + let mut tx = pool.begin().await?; + let id = path.into_inner(); - let featured = mods::is_featured(&id, &mut pool).await?; - let mut transaction = pool.begin().await.or(Err(ApiError::TransactionError))?; - if let Err(e) = Mod::update_mod(&id, payload.featured, &mut transaction).await { - transaction - .rollback() - .await - .or(Err(ApiError::TransactionError))?; - return Err(e); + + if !mods::exists(&id, &mut tx).await? { + return Err(ApiError::NotFound("Mod not found".into())); } - transaction - .commit() - .await - .or(Err(ApiError::TransactionError))?; + + let featured = mods::is_featured(&id, &mut tx).await?; + + Mod::update_mod(&id, payload.featured, &mut tx).await?; + + tx.commit().await?; if featured != payload.featured { let item = Mod::get_one(&id, true, &mut pool).await?; if let Some(item) = item { - let owner = developers::get_owner_for_mod(&id, &mut pool).await?; - let first_ver = item.versions.first(); - if let Some(ver) = first_ver { - ModFeaturedEvent { - id: item.id, - name: ver.name.clone(), - owner, - admin: dev, - base_url: data.app_url().to_string(), - featured: payload.featured, + if let Some(owner) = developers::get_owner_for_mod(&id, &mut pool).await? { + let first_ver = item.versions.first(); + if let Some(ver) = first_ver { + ModFeaturedEvent { + id: item.id, + name: ver.name.clone(), + owner, + admin: dev, + base_url: data.app_url().to_string(), + featured: payload.featured, + } + .to_discord_webhook() + .send(data.webhook_url()); } - .to_discord_webhook() - .send(data.webhook_url()); } } } diff --git a/src/endpoints/stats.rs b/src/endpoints/stats.rs index f1efd25..ebaebbc 100644 --- a/src/endpoints/stats.rs +++ b/src/endpoints/stats.rs @@ -1,18 +1,12 @@ use actix_web::{get, web, Responder}; +use super::ApiError; use crate::config::AppData; -use crate::types::{ - api::{ApiError, ApiResponse}, - models::stats::Stats, -}; +use crate::types::{api::ApiResponse, models::stats::Stats}; #[get("/v1/stats")] pub async fn get_stats(data: web::Data) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; Ok(web::Json(ApiResponse { error: "".into(), payload: Stats::get_cached(&mut pool).await?, diff --git a/src/endpoints/tags.rs b/src/endpoints/tags.rs index 3ef44a3..7eeb88e 100644 --- a/src/endpoints/tags.rs +++ b/src/endpoints/tags.rs @@ -2,15 +2,12 @@ use actix_web::{get, web, Responder}; use crate::config::AppData; use crate::database::repository::mod_tags; -use crate::types::api::{ApiError, ApiResponse}; +use crate::endpoints::ApiError; +use crate::types::api::ApiResponse; #[get("/v1/tags")] pub async fn index(data: web::Data) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let tags = mod_tags::get_all_writable(&mut pool) .await? .into_iter() @@ -25,11 +22,7 @@ pub async fn index(data: web::Data) -> Result #[get("/v1/detailed-tags")] pub async fn detailed_index(data: web::Data) -> Result { - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; + let mut pool = data.db().acquire().await?; let tags = mod_tags::get_all(&mut pool).await?; diff --git a/src/extractors/auth.rs b/src/extractors/auth.rs index 3f2418f..5e845d0 100644 --- a/src/extractors/auth.rs +++ b/src/extractors/auth.rs @@ -1,7 +1,10 @@ use std::pin::Pin; +use crate::auth::AuthenticationError; use crate::config::AppData; -use crate::types::{api::ApiError, models::developer::Developer}; +use crate::database::repository::developers; +use crate::endpoints::ApiError; +use crate::types::models::developer::Developer; use actix_web::http::header::HeaderMap; use actix_web::{web, FromRequest, HttpRequest}; use futures::Future; @@ -13,30 +16,39 @@ pub struct Auth { } impl Auth { - /** - * Returns Ok(developer) if token was valid in request or returns ApiError::Unauthorized otherwise - */ - pub fn developer(&self) -> Result { + pub fn developer(&self) -> Result { + if self.token.is_none() { + return Err(AuthenticationError::NoToken); + } match &self.developer { - None => Err(ApiError::Unauthorized), + None => Err(AuthenticationError::InvalidToken), Some(d) => Ok(d.clone()), } } - pub fn token(&self) -> Result { + fn check_auth(&self) -> Result<(), AuthenticationError> { + if self.token.is_none() { + return Err(AuthenticationError::NoToken); + } + if self.developer.is_none() { + return Err(AuthenticationError::InvalidToken); + } + + Ok(()) + } + + pub fn token(&self) -> Result { match self.token { - None => Err(ApiError::Unauthorized), + None => Err(AuthenticationError::NoToken), Some(t) => Ok(t), } } - pub fn admin(&self) -> Result<(), ApiError> { - if self.developer.is_none() { - return Err(ApiError::Unauthorized); - } + pub fn check_admin(&self) -> Result<(), ApiError> { + self.check_auth()?; match self.developer.as_ref().is_some_and(|dev| dev.admin) { - false => Err(ApiError::Forbidden), + false => Err(ApiError::Authorization), true => Ok(()), } } @@ -60,45 +72,17 @@ impl FromRequest for Auth { } }; - let mut pool = data - .db() - .acquire() - .await - .or(Err(ApiError::DbAcquireError))?; - let hash = sha256::digest(token.to_string()); - let developer = match sqlx::query_as!( - Developer, - "SELECT - d.id, - d.username, - d.display_name, - d.verified, - d.admin, - d.github_user_id as github_id - FROM developers d - INNER JOIN auth_tokens a ON d.id = a.developer_id - WHERE a.token = $1", - hash - ) - .fetch_optional(&mut *pool) - .await - .map_err(|e| { - log::error!("Failed to lookup developer for auth: {}", e); - ApiError::DbError - })? { - None => { - return Ok(Auth { - developer: None, - token: None, - }) - } - Some(d) => d, - }; - - Ok(Auth { - developer: Some(developer), - token: Some(token), - }) + let mut pool = data.db().acquire().await?; + match developers::find_by_token(&token, &mut pool).await? { + None => Ok(Auth { + developer: None, + token: Some(token), + }), + Some(dev) => Ok(Auth { + developer: Some(dev), + token: Some(token), + }), + } }) } } diff --git a/src/jobs/cleanup_downloads.rs b/src/jobs/cleanup_downloads.rs index 211ec20..ec9a42a 100644 --- a/src/jobs/cleanup_downloads.rs +++ b/src/jobs/cleanup_downloads.rs @@ -1,9 +1,10 @@ -use sqlx::PgConnection; use crate::database::repository::mod_downloads; -use crate::types::api::ApiError; +use crate::endpoints::ApiError; +use sqlx::PgConnection; pub async fn cleanup_downloads(conn: &mut PgConnection) -> Result<(), ApiError> { mod_downloads::cleanup(conn).await?; Ok(()) -} \ No newline at end of file +} + diff --git a/src/jobs/logout_user.rs b/src/jobs/logout_user.rs index aa249ae..c9b17d9 100644 --- a/src/jobs/logout_user.rs +++ b/src/jobs/logout_user.rs @@ -1,5 +1,5 @@ use crate::database::repository::{auth_tokens, developers, refresh_tokens}; -use crate::types::api::ApiError; +use crate::endpoints::ApiError; use sqlx::PgConnection; pub async fn logout_user(username: &str, conn: &mut PgConnection) -> Result<(), ApiError> { diff --git a/src/jobs/token_cleanup.rs b/src/jobs/token_cleanup.rs index 70d97f4..00ef8b6 100644 --- a/src/jobs/token_cleanup.rs +++ b/src/jobs/token_cleanup.rs @@ -1,6 +1,6 @@ -use sqlx::PgConnection; use crate::database::repository::{auth_tokens, refresh_tokens}; -use crate::types::api::ApiError; +use crate::endpoints::ApiError; +use sqlx::PgConnection; pub async fn token_cleanup(conn: &mut PgConnection) -> Result<(), ApiError> { auth_tokens::cleanup(conn).await?; diff --git a/src/main.rs b/src/main.rs index d9349eb..4359cef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ use actix_web::{ }; use crate::types::api; - mod auth; mod cli; mod config; diff --git a/src/mod_zip.rs b/src/mod_zip.rs index 3350f3f..3e95f5c 100644 --- a/src/mod_zip.rs +++ b/src/mod_zip.rs @@ -4,37 +4,58 @@ use std::io::{BufReader, Cursor, Read}; use actix_web::web::Bytes; use image::codecs::png::PngDecoder; use image::codecs::png::PngEncoder; -use image::ImageEncoder; use image::{DynamicImage, GenericImageView}; +use image::{ImageEncoder, ImageError}; use zip::read::ZipFile; +use zip::result::ZipError; use zip::ZipArchive; -use crate::types::api::ApiError; +#[derive(thiserror::Error, Debug)] +pub enum ModZipError { + #[error("I/O error: {0}")] + IoError(#[from] std::io::Error), + #[error("Image operation error: {0}")] + ImageError(#[from] ImageError), + #[error("Failed to unzip .geode file: {0}")] + ZipError(#[from] ZipError), + #[error("Failed to parse JSON: {0}")] + SerdeJsonError(#[from] serde_json::Error), + #[error("Invalid mod logo: {0}")] + InvalidLogo(String), + #[error(".geode file hash mismatch: {0} doesn't match {1}")] + ModFileHashMismatch(String, String), + #[error("Failed to fetch .geode file: {0}")] + ModFileFetchError(#[from] reqwest::Error), + #[error(".geode file is too large ({0} MB), maximum is {1} MB")] + ModFileTooLarge(u64, u64), + #[error("Invalid mod.json: {0}")] + InvalidModJson(String), + #[error("Invalid binaries: {0}")] + InvalidBinaries(String), +} -pub fn extract_mod_logo(file: &mut ZipFile>) -> Result, ApiError> { +pub fn extract_mod_logo(file: &mut ZipFile>) -> Result, ModZipError> { const FIVE_MEGABYTES: u64 = 5 * 1000 * 1000; if file.size() > FIVE_MEGABYTES { - return Err(ApiError::BadRequest( + return Err(ModZipError::InvalidLogo( "Logo size excedes max allowed size (5 MB)".into(), )); } let mut logo: Vec = Vec::with_capacity(file.size() as usize); file.read_to_end(&mut logo) - .inspect_err(|e| log::error!("logo.png read fail: {}", e)) - .or(Err(ApiError::BadRequest("Couldn't read logo.png".into())))?; + .inspect_err(|e| log::error!("logo.png read fail: {}", e))?; let mut reader = BufReader::new(Cursor::new(logo)); let mut img = PngDecoder::new(&mut reader) .and_then(DynamicImage::from_decoder) - .inspect_err(|e| log::error!("Failed to create PngDecoder: {}", e)) - .or(Err(ApiError::BadRequest("Invalid logo.png".into())))?; + .inspect_err(|e| log::error!("Failed to create PngDecoder: {}", e))?; let dimensions = img.dimensions(); if dimensions.0 != dimensions.1 { - return Err(ApiError::BadRequest(format!( + return Err(ModZipError::InvalidLogo(format!( "Mod logo must have 1:1 aspect ratio. Current size is {}x{}", dimensions.0, dimensions.1 ))); @@ -56,8 +77,7 @@ pub fn extract_mod_logo(file: &mut ZipFile>) -> Result, Ap encoder .write_image(img.as_bytes(), width, height, img.color().into()) - .inspect_err(|e| log::error!("Failed to downscale image to 336x336: {}", e)) - .or(Err(ApiError::BadRequest("Invalid mod.json".into())))?; + .inspect_err(|e| log::error!("Failed to downscale image to 336x336: {}", e))?; cursor.seek(std::io::SeekFrom::Start(0)).unwrap(); @@ -67,30 +87,28 @@ pub fn extract_mod_logo(file: &mut ZipFile>) -> Result, Ap Ok(bytes) } -pub fn validate_mod_logo(file: &mut ZipFile>) -> Result<(), ApiError> { +pub fn validate_mod_logo(file: &mut ZipFile>) -> Result<(), ModZipError> { const FIVE_MEGABYTES: u64 = 5 * 1000 * 1000; if file.size() > FIVE_MEGABYTES { - return Err(ApiError::BadRequest( + return Err(ModZipError::InvalidLogo( "Logo size excedes max allowed size (5 MB)".into(), )); } let mut logo: Vec = Vec::with_capacity(file.size() as usize); file.read_to_end(&mut logo) - .inspect_err(|e| log::error!("logo.png read fail: {}", e)) - .or(Err(ApiError::BadRequest("Couldn't read logo.png".into())))?; + .inspect_err(|e| log::error!("logo.png read fail: {}", e))?; let mut reader = BufReader::new(Cursor::new(logo)); let img = PngDecoder::new(&mut reader) .and_then(DynamicImage::from_decoder) - .inspect_err(|e| log::error!("Failed to create PngDecoder: {}", e)) - .or(Err(ApiError::BadRequest("Invalid logo.png".into())))?; + .inspect_err(|e| log::error!("Failed to create PngDecoder: {}", e))?; let dimensions = img.dimensions(); if dimensions.0 != dimensions.1 { - Err(ApiError::BadRequest(format!( + Err(ModZipError::InvalidLogo(format!( "Mod logo must have 1:1 aspect ratio. Current size is {}x{}", dimensions.0, dimensions.1 ))) @@ -99,7 +117,7 @@ pub fn validate_mod_logo(file: &mut ZipFile>) -> Result<(), ApiErr } } -pub async fn download_mod(url: &str, limit_mb: u32) -> Result { +pub async fn download_mod(url: &str, limit_mb: u32) -> Result { download(url, limit_mb).await } @@ -107,37 +125,30 @@ pub async fn download_mod_hash_comp( url: &str, hash: &str, limit_mb: u32, -) -> Result { +) -> Result { let bytes = download(url, limit_mb).await?; let slice: &[u8] = &bytes; let new_hash = sha256::digest(slice); if new_hash != hash { - return Err(ApiError::BadRequest(format!( - ".geode hash mismatch: old {hash}, new {new_hash}", - ))); + return Err(ModZipError::ModFileHashMismatch(hash.into(), new_hash)); } Ok(bytes) } -pub fn bytes_to_ziparchive(bytes: Bytes) -> Result>, ApiError> { +pub fn bytes_to_ziparchive(bytes: Bytes) -> Result>, ModZipError> { ZipArchive::new(Cursor::new(bytes)) .inspect_err(|e| log::error!("Failed to create ZipArchive: {}", e)) - .or(Err(ApiError::BadRequest( - "Invalid .geode file, couldn't read archive".into(), - ))) + .map_err(|e| e.into()) } -async fn download(url: &str, limit_mb: u32) -> Result { +async fn download(url: &str, limit_mb: u32) -> Result { let limit_bytes: u64 = limit_mb as u64 * 1_000_000; let mut response = reqwest::get(url) .await - .inspect_err(|e| log::error!("Failed to fetch .geode file: {e}")) - .or(Err(ApiError::BadRequest( - "Failed to fetch .geode file".into(), - )))?; + .inspect_err(|e| log::error!("Failed to fetch .geode file: {e}"))?; // Check Content-Length, but the server can lie about this, so we'll also stream the file // If the header is somehow unavailable, we'll just check the size when streaming @@ -145,26 +156,18 @@ async fn download(url: &str, limit_mb: u32) -> Result { if content_length > limit_bytes { let len_mb = content_length / 1_000_000; - return Err(ApiError::BadRequest(format!( - "Mod file is too large ({} mb), max size is {} mb", - len_mb, limit_mb - ))); + return Err(ModZipError::ModFileTooLarge(len_mb, limit_mb.into())); } let mut data: Vec = Vec::with_capacity(content_length as usize); let mut streamed: u64 = 0; - while let Some(chunk) = response.chunk().await.or(Err(ApiError::BadRequest( - "Failed to read .geode chunk".into(), - )))? { + while let Some(chunk) = response.chunk().await? { streamed += chunk.len() as u64; if streamed > limit_bytes { let len_mb = streamed / 1_000_000; - return Err(ApiError::BadRequest(format!( - "Mod file is too large ({} mb), max size is {} mb", - len_mb, limit_mb - ))); + return Err(ModZipError::ModFileTooLarge(len_mb, limit_mb.into())); } data.extend_from_slice(&chunk); diff --git a/src/types/api.rs b/src/types/api.rs index 3d6a307..9ef7087 100644 --- a/src/types/api.rs +++ b/src/types/api.rs @@ -1,8 +1,7 @@ -use std::error::Error; -use actix_web::{error::QueryPayloadError, http::header::ContentType, HttpRequest, HttpResponse}; +use actix_web::{error::QueryPayloadError, HttpRequest}; use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use actix_web::http::StatusCode; + +use crate::endpoints::ApiError; #[derive(Serialize, Deserialize)] pub struct PaginatedData { @@ -10,67 +9,12 @@ pub struct PaginatedData { pub count: i64, } -#[derive(Debug, PartialEq)] -pub enum ApiError { - FilesystemError, - DbAcquireError, - DbError, - TransactionError, - InternalError, - BadRequest(String), - NotFound(String), - Unauthorized, - Forbidden, -} - #[derive(Debug, Serialize, Deserialize)] pub struct ApiResponse { pub error: String, pub payload: T, } -impl Error for ApiError {} - -impl Display for ApiError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::FilesystemError => write!(f, "Unknown filesystem error"), - Self::DbAcquireError => write!(f, "Database is busy"), - Self::DbError => write!(f, "Unknown database error"), - Self::TransactionError => write!(f, "Unknown transaction error"), - Self::BadRequest(message) => write!(f, "{}", message), - Self::NotFound(message) => write!(f, "{}", message), - Self::InternalError => write!(f, "Internal server error"), - Self::Forbidden => write!(f, "You cannot perform this action"), - Self::Unauthorized => write!(f, "You need to be authenticated to perform this action"), - } - } -} - -impl actix_web::ResponseError for ApiError { - fn status_code(&self) -> StatusCode { - match self { - Self::FilesystemError => StatusCode::INTERNAL_SERVER_ERROR, - Self::DbAcquireError => StatusCode::INTERNAL_SERVER_ERROR, - Self::DbError => StatusCode::INTERNAL_SERVER_ERROR, - Self::TransactionError => StatusCode::INTERNAL_SERVER_ERROR, - Self::BadRequest(_) => StatusCode::BAD_REQUEST, - Self::NotFound(_) => StatusCode::NOT_FOUND, - Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR, - Self::Unauthorized => StatusCode::UNAUTHORIZED, - Self::Forbidden => StatusCode::FORBIDDEN, - } - } - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()) - .append_header(ContentType::json()) - .json(ApiResponse { - error: self.to_string(), - payload: "".to_string(), - }) - } -} - pub fn query_error_handler(err: QueryPayloadError, _req: &HttpRequest) -> actix_web::Error { ApiError::BadRequest(err.to_string()).into() } diff --git a/src/types/mod_json.rs b/src/types/mod_json.rs index 037b3b2..f54af57 100644 --- a/src/types/mod_json.rs +++ b/src/types/mod_json.rs @@ -8,15 +8,12 @@ use semver::Version; use serde::Deserialize; use zip::read::ZipFile; -use crate::mod_zip; - -use super::{ - api::ApiError, - models::{ - dependency::{DependencyCreate, DependencyImportance, ModVersionCompare}, - incompatibility::{IncompatibilityCreate, IncompatibilityImportance}, - mod_gd_version::DetailedGDVersion, - }, +use crate::mod_zip::{self, ModZipError}; + +use super::models::{ + dependency::{DependencyCreate, DependencyImportance, ModVersionCompare}, + incompatibility::{IncompatibilityCreate, IncompatibilityImportance}, + mod_gd_version::DetailedGDVersion, }; #[derive(Debug, Deserialize)] @@ -129,18 +126,19 @@ impl ModJson { file: Bytes, download_url: &str, store_image: bool, - ) -> Result { + ) -> Result { let slice: &[u8] = &file; let hash = sha256::digest(slice); let mut archive = mod_zip::bytes_to_ziparchive(file)?; let json_file = archive .by_name("mod.json") - .or(Err(ApiError::BadRequest("mod.json not found".into())))?; + .or(Err(ModZipError::InvalidModJson( + "No mod.json found in .geode file".into(), + )))?; let mut json = serde_json::from_reader::>, ModJson>(json_file) - .inspect_err(|e| log::error!("Failed to parse mod.json: {}", e)) - .or(Err(ApiError::BadRequest("Invalid mod.json".into())))?; + .inspect_err(|e| log::error!("Failed to parse mod.json: {e}"))?; json.version = json.version.trim_start_matches('v').to_string(); json.hash = hash; @@ -161,15 +159,23 @@ impl ModJson { } else if file.name().ends_with(".android64.so") { json.android64 = true; } else if file.name().eq("about.md") { - json.about = Some(parse_zip_entry_to_str(&mut file).map_err(|e| { - log::error!("Failed to parse about.md for mod: {}", e); - ApiError::InternalError - })?); + json.about = Some( + parse_zip_entry_to_str(&mut file) + .inspect_err(|e| log::error!("Failed to parse about.md for mod: {e}")) + .map_err(|e| { + ModZipError::InvalidModJson(format!("Failed to read about.md: {e}")) + })?, + ); } else if file.name().eq("changelog.md") { - json.changelog = Some(parse_zip_entry_to_str(&mut file).map_err(|e| { - log::error!("Failed to parse changelog.md for mod: {}", e); - ApiError::InternalError - })?); + json.changelog = Some( + parse_zip_entry_to_str(&mut file) + .inspect_err(|e| log::error!("Failed to parse changelog.md: {e}")) + .map_err(|e| { + ModZipError::InvalidModJson(format!( + "Failed to read changelog.md: {e}" + )) + })?, + ); } else if file.name() == "logo.png" { if store_image { json.logo = mod_zip::extract_mod_logo(&mut file)?; @@ -182,7 +188,7 @@ impl ModJson { Ok(json) } - pub fn prepare_dependencies_for_create(&self) -> Result, ApiError> { + pub fn prepare_dependencies_for_create(&self) -> Result, ModZipError> { let deps = match self.dependencies.as_ref() { None => return Ok(vec![]), Some(d) => d, @@ -208,7 +214,7 @@ impl ModJson { } let (dependency_ver, compare) = split_version_and_compare(&(i.version)) .map_err(|_| { - ApiError::BadRequest(format!("Invalid semver {}", i.version)) + ModZipError::InvalidModJson(format!("Invalid semver {}", i.version)) })?; ret.push(DependencyCreate { dependency_id: i.id.clone(), @@ -231,7 +237,10 @@ impl ModJson { ModJsonDependencyType::Version(version) => { let (dependency_ver, compare) = split_version_and_compare(version) .map_err(|_| { - ApiError::BadRequest(format!("Invalid semver {}", version)) + ModZipError::InvalidModJson(format!( + "Invalid semver {}", + version + )) })?; ret.push(DependencyCreate { dependency_id: id.clone(), @@ -243,7 +252,7 @@ impl ModJson { ModJsonDependencyType::Detailed(detailed) => { let (dependency_ver, compare) = split_version_and_compare(&(detailed.version)).map_err(|_| { - ApiError::BadRequest(format!( + ModZipError::InvalidModJson(format!( "Invalid semver {}", detailed.version )) @@ -264,7 +273,7 @@ impl ModJson { pub fn prepare_incompatibilities_for_create( &self, - ) -> Result, ApiError> { + ) -> Result, ModZipError> { let incompat = match self.incompatibilities.as_ref() { None => return Ok(vec![]), Some(d) => d, @@ -290,7 +299,7 @@ impl ModJson { } let (ver, compare) = split_version_and_compare(&(i.version)).map_err(|_| { - ApiError::BadRequest(format!("Invalid semver: {}", i.version)) + ModZipError::InvalidModJson(format!("Invalid semver: {}", i.version)) })?; ret.push(IncompatibilityCreate { incompatibility_id: i.id.clone(), @@ -314,7 +323,10 @@ impl ModJson { ModJsonIncompatibilityType::Version(version) => { let (ver, compare) = split_version_and_compare(version).map_err(|_| { - ApiError::BadRequest(format!("Invalid semver {}", version)) + ModZipError::InvalidModJson(format!( + "Invalid semver {}", + version + )) })?; ret.push(IncompatibilityCreate { incompatibility_id: id.clone(), @@ -326,7 +338,7 @@ impl ModJson { ModJsonIncompatibilityType::Detailed(detailed) => { let (ver, compare) = split_version_and_compare(&(detailed.version)) .map_err(|_| { - ApiError::BadRequest(format!( + ModZipError::InvalidModJson(format!( "Invalid semver {}", detailed.version )) @@ -346,37 +358,37 @@ impl ModJson { } } - pub fn validate(&self) -> Result<(), ApiError> { + pub fn validate(&self) -> Result<(), ModZipError> { let id_regex = Regex::new(r#"^[a-z0-9_\-]+\.[a-z0-9_\-]+$"#).unwrap(); if !id_regex.is_match(&self.id) { - return Err(ApiError::BadRequest(format!( + return Err(ModZipError::InvalidModJson(format!( "Invalid mod id {} (lowercase and numbers only, needs to look like 'dev.mod')", self.id ))); } if Version::parse(self.version.trim_start_matches('v')).is_err() { - return Err(ApiError::BadRequest(format!( + return Err(ModZipError::InvalidModJson(format!( "Invalid mod.json mod version: {}", self.version ))); }; if Version::parse(self.geode.trim_start_matches('v')).is_err() { - return Err(ApiError::BadRequest(format!( + return Err(ModZipError::InvalidModJson(format!( "Invalid mod.json geode version: {}", self.geode ))); }; if self.developer.is_none() && self.developers.is_none() { - return Err(ApiError::BadRequest( + return Err(ModZipError::InvalidModJson( "No developer specified on mod.json".to_string(), )); } if self.id.len() > 64 { - return Err(ApiError::BadRequest( + return Err(ModZipError::InvalidModJson( "Mod id too long (max 64 characters)".to_string(), )); } @@ -384,7 +396,7 @@ impl ModJson { if let Some(l) = &self.links { if let Some(community) = &l.community { if let Err(e) = Url::parse(community) { - return Err(ApiError::BadRequest(format!( + return Err(ModZipError::InvalidModJson(format!( "Invalid community URL: {}. Reason: {}", community, e ))); @@ -392,7 +404,7 @@ impl ModJson { } if let Some(homepage) = &l.homepage { if let Err(e) = Url::parse(homepage) { - return Err(ApiError::BadRequest(format!( + return Err(ModZipError::InvalidModJson(format!( "Invalid homepage URL: {}. Reason: {}", homepage, e ))); @@ -400,7 +412,7 @@ impl ModJson { } if let Some(source) = &l.source { if let Err(e) = Url::parse(source) { - return Err(ApiError::BadRequest(format!( + return Err(ModZipError::InvalidModJson(format!( "Invalid source URL: {}. Reason: {}", source, e ))); @@ -453,12 +465,12 @@ fn parse_download_url(url: &str) -> String { String::from(url.trim_end_matches("\\/")) } -fn check_mac_binary(file: &mut ZipFile>) -> Result<(bool, bool), ApiError> { +fn check_mac_binary(file: &mut ZipFile>) -> Result<(bool, bool), ModZipError> { // 12 bytes is all we need let mut bytes: Vec = vec![0; 12]; file.read_exact(&mut bytes).map_err(|e| { log::error!("Failed to read MacOS binary: {}", e); - ApiError::BadRequest("Invalid MacOS binary".into()) + ModZipError::InvalidBinaries(format!("Failed to read macOS binary: {e}")) })?; // Information taken from: https://www.jviotti.com/2021/07/23/a-deep-dive-on-macos-universal-binaries.html and some simple xxd fuckery @@ -492,12 +504,12 @@ fn check_mac_binary(file: &mut ZipFile>) -> Result<(bool, bool), A return Ok((true, false)); } else { // probably invalid - return Err(ApiError::BadRequest("Invalid MacOS binary".to_string())); + return Err(ModZipError::InvalidBinaries("Invalid macOS binary".into())); } } else if num_arches == 0x2 { return Ok((true, true)); } else { - return Err(ApiError::BadRequest("Invalid MacOS binary".to_string())); + return Err(ModZipError::InvalidBinaries("Invalid macOS binary".into())); } } else if is_single_platform { let first = bytes[4]; @@ -510,5 +522,5 @@ fn check_mac_binary(file: &mut ZipFile>) -> Result<(bool, bool), A return Ok((true, false)); } } - Err(ApiError::BadRequest("Invalid MacOS binary".to_string())) + Err(ModZipError::InvalidBinaries("Invalid macOS binary".into())) } diff --git a/src/types/models/dependency.rs b/src/types/models/dependency.rs index b456e13..e477106 100644 --- a/src/types/models/dependency.rs +++ b/src/types/models/dependency.rs @@ -2,8 +2,7 @@ use std::{collections::HashMap, fmt::Display}; use serde::{Deserialize, Serialize}; use sqlx::PgConnection; - -use crate::types::api::ApiError; +use crate::database::DatabaseError; use super::mod_gd_version::{GDVersionEnum, VerPlatform}; @@ -111,7 +110,7 @@ impl Dependency { gd: Option, geode: Option<&semver::Version>, pool: &mut PgConnection, - ) -> Result>, ApiError> { + ) -> Result>, DatabaseError> { // Fellow developer, I am sorry for what you're about to see :) // I present to you the ugly monster of the Geode index // The *GigaQueryâ„¢* @@ -262,8 +261,7 @@ impl Dependency { .bind(geode_pre) .fetch_all(&mut *pool) .await - .inspect_err(|x| log::error!("Failed to fetch dependencies: {}", x)) - .or(Err(ApiError::DbError))?; + .inspect_err(|x| log::error!("Failed to fetch dependencies: {x}"))?; let mut ret: HashMap> = HashMap::new(); for i in result { diff --git a/src/types/models/gd_version_alias.rs b/src/types/models/gd_version_alias.rs index 2e8de3b..f800f25 100644 --- a/src/types/models/gd_version_alias.rs +++ b/src/types/models/gd_version_alias.rs @@ -1,96 +1,89 @@ -use crate::types::{ - models::mod_gd_version::{GDVersionEnum, VerPlatform}, - api::ApiError, +use crate::{ + database::DatabaseError, + types::models::mod_gd_version::{GDVersionEnum, VerPlatform}, }; use serde::Serialize; -use sqlx::{ - types::Uuid, - PgConnection, Postgres, QueryBuilder -}; +use sqlx::{types::Uuid, PgConnection, Postgres, QueryBuilder}; #[derive(Serialize)] pub struct GDVersionAlias { - pub version_name: GDVersionEnum, - pub mac_arm_uuid: Option, - pub mac_intel_uuid: Option, - pub android_manifest_id: Option, - pub windows_timestamp: Option, - pub ios_bundle_version: Option, - pub added_at: String + pub version_name: GDVersionEnum, + pub mac_arm_uuid: Option, + pub mac_intel_uuid: Option, + pub android_manifest_id: Option, + pub windows_timestamp: Option, + pub ios_bundle_version: Option, + pub added_at: String, } impl GDVersionAlias { - pub async fn find( - platform: VerPlatform, - identifier: &str, - pool: &mut PgConnection, - ) -> Result { - let mut query_builder: QueryBuilder = QueryBuilder::new( - r#"SELECT version_name FROM gd_version_aliases"# - ); + pub async fn find( + platform: VerPlatform, + identifier: &str, + pool: &mut PgConnection, + ) -> Result, DatabaseError> { + let mut query_builder: QueryBuilder = + QueryBuilder::new(r#"SELECT version_name FROM gd_version_aliases"#); - match platform { - VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => { - let manifest_id = identifier.parse::() - .map_err(|_| ApiError::BadRequest("Identifier is not a valid manifest id".to_string()))?; + match platform { + VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => { + let manifest_id = identifier.parse::().map_err(|_| { + DatabaseError::InvalidInput("Identifier is not a valid manifest id".to_string()) + })?; - query_builder.push(" WHERE android_manifest_id="); - query_builder.push_bind(manifest_id); - }, - VerPlatform::Mac => { - let uuid = Uuid::parse_str(identifier) - .map_err(|_| ApiError::BadRequest("Identifier is not a valid UUID".to_string()))?; + query_builder.push(" WHERE android_manifest_id="); + query_builder.push_bind(manifest_id); + } + VerPlatform::Mac => { + let uuid = Uuid::parse_str(identifier).map_err(|_| { + DatabaseError::InvalidInput("Identifier is not a valid UUID".to_string()) + })?; - query_builder.push(" WHERE mac_arm_uuid="); - query_builder.push_bind(uuid); - query_builder.push(" OR mac_intel_uuid="); - query_builder.push_bind(uuid); - }, - VerPlatform::MacArm => { - let uuid = Uuid::parse_str(identifier) - .map_err(|_| ApiError::BadRequest("Identifier is not a valid UUID".to_string()))?; + query_builder.push(" WHERE mac_arm_uuid="); + query_builder.push_bind(uuid); + query_builder.push(" OR mac_intel_uuid="); + query_builder.push_bind(uuid); + } + VerPlatform::MacArm => { + let uuid = Uuid::parse_str(identifier).map_err(|_| { + DatabaseError::InvalidInput("Identifier is not a valid UUID".to_string()) + })?; - query_builder.push(" WHERE mac_arm_uuid="); - query_builder.push_bind(uuid); - }, - VerPlatform::MacIntel => { - let uuid = Uuid::parse_str(identifier) - .map_err(|_| ApiError::BadRequest("Identifier is not a valid UUID".to_string()))?; + query_builder.push(" WHERE mac_arm_uuid="); + query_builder.push_bind(uuid); + } + VerPlatform::MacIntel => { + let uuid = Uuid::parse_str(identifier).map_err(|_| { + DatabaseError::InvalidInput("Identifier is not a valid UUID".to_string()) + })?; - query_builder.push(" WHERE mac_intel_uuid="); - query_builder.push_bind(uuid); - }, - VerPlatform::Win => { - let timestamp = identifier.parse::() - .map_err(|_| ApiError::BadRequest("Identifier is not a valid timestamp".to_string()))?; + query_builder.push(" WHERE mac_intel_uuid="); + query_builder.push_bind(uuid); + } + VerPlatform::Win => { + let timestamp = identifier.parse::().map_err(|_| { + DatabaseError::InvalidInput("Identifier is not a valid timestamp".to_string()) + })?; - query_builder.push(" WHERE windows_timestamp="); - query_builder.push_bind(timestamp); - }, - VerPlatform::Ios => { - let bundle_version = identifier.to_string(); - query_builder.push(" WHERE ios_bundle_version="); - query_builder.push_bind(bundle_version); - } - // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), - }; + query_builder.push(" WHERE windows_timestamp="); + query_builder.push_bind(timestamp); + } + VerPlatform::Ios => { + let bundle_version = identifier.to_string(); + query_builder.push(" WHERE ios_bundle_version="); + query_builder.push_bind(bundle_version); + } // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), + }; - // probably useless? - query_builder.push(" ORDER BY added_at DESC LIMIT 1"); + // probably useless? + query_builder.push(" ORDER BY added_at DESC LIMIT 1"); - match query_builder - .build_query_scalar::() - .fetch_optional(&mut *pool) - .await - { - Ok(Some(v)) => Ok(v), - Ok(None) => Err(ApiError::NotFound("".to_string())), - Err(e) => { - log::error!("{:?}", e); - Err(ApiError::DbError) - } - } - } + query_builder + .build_query_scalar::() + .fetch_optional(&mut *pool) + .await + .map_err(|e| e.into()) + } } diff --git a/src/types/models/incompatibility.rs b/src/types/models/incompatibility.rs index 9434b38..f44ab5d 100644 --- a/src/types/models/incompatibility.rs +++ b/src/types/models/incompatibility.rs @@ -1,14 +1,13 @@ use std::collections::HashMap; -use crate::types::api::ApiError; -use crate::types::models::dependency::ModVersionCompare; -use serde::{Deserialize, Serialize}; -use sqlx::{PgConnection, Postgres}; - use super::{ dependency::ResponseDependency, mod_gd_version::{GDVersionEnum, VerPlatform}, }; +use crate::database::DatabaseError; +use crate::types::models::dependency::ModVersionCompare; +use serde::{Deserialize, Serialize}; +use sqlx::{PgConnection, Postgres}; #[derive(sqlx::FromRow, Clone, Debug)] pub struct FetchedIncompatibility { @@ -91,8 +90,8 @@ impl Incompatibility { pub async fn get_for_mod_version( id: i32, pool: &mut PgConnection, - ) -> Result, ApiError> { - match sqlx::query_as!( + ) -> Result, DatabaseError> { + sqlx::query_as!( FetchedIncompatibility, r#"SELECT icp.compare as "compare: _", icp.importance as "importance: _", @@ -103,13 +102,8 @@ impl Incompatibility { ) .fetch_all(&mut *pool) .await - { - Ok(d) => Ok(d), - Err(e) => { - log::error!("{}", e); - Err(ApiError::DbError) - } - } + .inspect_err(|e| log::error!("Failed to fetch incompatibilities for mod_version {id}: {e}")) + .map_err(|e| e.into()) } pub async fn get_for_mod_versions( @@ -118,15 +112,14 @@ impl Incompatibility { gd: Option, geode: Option<&semver::Version>, pool: &mut PgConnection, - ) -> Result>, ApiError> { - let geode_pre = geode - .and_then(|x| { - if x.pre.is_empty() { - None - } else { - Some(x.pre.to_string()) - } - }); + ) -> Result>, DatabaseError> { + let geode_pre = geode.and_then(|x| { + if x.pre.is_empty() { + None + } else { + Some(x.pre.to_string()) + } + }); let q = sqlx::query_as::( r#"SELECT icp.compare, @@ -164,13 +157,10 @@ impl Incompatibility { .bind(geode.map(|x| i64::try_from(x.patch).ok())) .bind(geode_pre); - let result = match q.fetch_all(&mut *pool).await { - Ok(d) => d, - Err(e) => { - log::error!("{}", e); - return Err(ApiError::DbError); - } - }; + let result = q.fetch_all(&mut *pool).await.inspect_err(|e| { + log::error!("Failed to fetch incompatibilities for mod_versions: {e}") + })?; + let mut ret: HashMap> = HashMap::new(); for i in result { @@ -186,14 +176,14 @@ impl Incompatibility { gd: GDVersionEnum, geode: &semver::Version, pool: &mut PgConnection, - ) -> Result, ApiError> { + ) -> Result, DatabaseError> { let mut ret: HashMap = HashMap::new(); let pre = if geode.pre.is_empty() { None } else { Some(geode.pre.to_string()) }; - let r = match sqlx::query!( + let r = sqlx::query!( r#" SELECT q.replaced, @@ -251,13 +241,7 @@ impl Incompatibility { ) .fetch_all(&mut *pool) .await - { - Err(e) => { - log::error!("Failed to fetch supersedes. ERR: {}", e); - return Err(ApiError::DbError); - } - Ok(r) => r, - }; + .inspect_err(|e| log::error!("Failed to fetch supersedes: {e}"))?; // Client doesn't actually use those, we might as well not return them yet // TODO: enable back when client supports then diff --git a/src/types/models/loader_version.rs b/src/types/models/loader_version.rs index 28ce9e6..5d3d8ae 100644 --- a/src/types/models/loader_version.rs +++ b/src/types/models/loader_version.rs @@ -1,279 +1,287 @@ -use crate::types::{ - models::mod_gd_version::{GDVersionEnum, VerPlatform, DetailedGDVersion}, - api::ApiError, +use crate::{ + database::DatabaseError, + types::models::mod_gd_version::{DetailedGDVersion, GDVersionEnum, VerPlatform}, }; use chrono::SecondsFormat; use serde::Serialize; use sqlx::{ - types::chrono::{DateTime, Utc}, - PgConnection, Postgres, QueryBuilder + types::chrono::{DateTime, Utc}, + PgConnection, Postgres, QueryBuilder, }; #[derive(Debug)] pub struct LoaderVersionCreate { - pub tag: String, - pub prerelease: bool, - pub commit_hash: String, - pub mac: Option, - pub win: Option, - pub android: Option, - pub ios: Option, + pub tag: String, + pub prerelease: bool, + pub commit_hash: String, + pub mac: Option, + pub win: Option, + pub android: Option, + pub ios: Option, } #[derive(Serialize, Debug)] pub struct LoaderVersion { - pub version: String, - pub tag: String, - pub gd: DetailedGDVersion, - pub prerelease: bool, - pub commit_hash: String, - pub created_at: String, + pub version: String, + pub tag: String, + pub gd: DetailedGDVersion, + pub prerelease: bool, + pub commit_hash: String, + pub created_at: String, } #[derive(sqlx::FromRow, Debug)] pub struct LoaderVersionGetOne { - pub tag: String, - pub prerelease: bool, - pub commit_hash: String, - pub created_at: DateTime, - pub mac: Option, - pub win: Option, - pub android: Option, - pub ios: Option, + pub tag: String, + pub prerelease: bool, + pub commit_hash: String, + pub created_at: DateTime, + pub mac: Option, + pub win: Option, + pub android: Option, + pub ios: Option, } pub struct GetVersionsQuery { - pub gd: Option, - pub platform: Option, - pub prerelease: bool + pub gd: Option, + pub platform: Option, + pub prerelease: bool, } impl LoaderVersionGetOne { - pub fn into_loader_version(self) -> LoaderVersion { - LoaderVersion { - tag: format!("v{}", self.tag), - version: self.tag, - prerelease: self.prerelease, - created_at: self.created_at.to_rfc3339_opts(SecondsFormat::Secs, true), - commit_hash: self.commit_hash, - gd: DetailedGDVersion { - win: self.win, - mac: self.mac, - mac_arm: self.mac, - mac_intel: self.mac, - android: self.android, - android32: self.android, - android64: self.android, - ios: self.ios, - } - } - } + pub fn into_loader_version(self) -> LoaderVersion { + LoaderVersion { + tag: format!("v{}", self.tag), + version: self.tag, + prerelease: self.prerelease, + created_at: self.created_at.to_rfc3339_opts(SecondsFormat::Secs, true), + commit_hash: self.commit_hash, + gd: DetailedGDVersion { + win: self.win, + mac: self.mac, + mac_arm: self.mac, + mac_intel: self.mac, + android: self.android, + android32: self.android, + android64: self.android, + ios: self.ios, + }, + } + } } impl LoaderVersion { - pub async fn get_latest( - gd: Option, - platform: Option, - accept_prereleases: bool, - pool: &mut PgConnection, - ) -> Result { - let mut query_builder: QueryBuilder = QueryBuilder::new( - r#"SELECT - mac, win, android, ios, tag, commit_hash, created_at, prerelease - FROM geode_versions"# - ); + pub async fn get_latest( + gd: Option, + platform: Option, + accept_prereleases: bool, + pool: &mut PgConnection, + ) -> Result, DatabaseError> { + let mut query_builder: QueryBuilder = QueryBuilder::new( + r#"SELECT + mac, win, android, ios, tag, commit_hash, created_at, prerelease + FROM geode_versions + "#, + ); - match (platform, gd) { - (Some(p), Some(g)) => { - match p { - VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => query_builder.push(" WHERE android="), - VerPlatform::Mac | VerPlatform::MacIntel | VerPlatform::MacArm => query_builder.push(" WHERE mac="), - VerPlatform::Ios => query_builder.push(" WHERE ios="), - VerPlatform::Win => query_builder.push(" WHERE win="), - // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), - }; + match (platform, gd) { + (Some(p), Some(g)) => { + match p { + VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => { + query_builder.push(" WHERE android=") + } + VerPlatform::Mac | VerPlatform::MacIntel | VerPlatform::MacArm => { + query_builder.push(" WHERE mac=") + } + VerPlatform::Ios => query_builder.push(" WHERE ios="), + VerPlatform::Win => query_builder.push(" WHERE win="), + // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), + }; - query_builder.push_bind(g); - } - (Some(_), None) => { - // this option will be handled later by ordering tricks - query_builder.push(" WHERE 1=1"); - } - (None, Some(g)) => { - query_builder.push(" WHERE android="); - query_builder.push_bind(g); - query_builder.push(" or mac="); - query_builder.push_bind(g); - query_builder.push(" or win="); - query_builder.push_bind(g); - query_builder.push( " or ios="); - query_builder.push_bind(g); - } - (None, None) => { - // if gd version isn't specifed, select whatever versions have the latest gd version - query_builder.push( - r#" WHERE - android=enum_last(NULL::gd_version) OR - win=enum_last(NULL::gd_version) OR - mac=enum_last(NULL::gd_version) OR - ios=enum_last(NULL::gd_version) - "#); - } - } + query_builder.push_bind(g); + } + (Some(_), None) => { + // this option will be handled later by ordering tricks + query_builder.push(" WHERE 1=1"); + } + (None, Some(g)) => { + query_builder.push(" WHERE android="); + query_builder.push_bind(g); + query_builder.push(" or mac="); + query_builder.push_bind(g); + query_builder.push(" or win="); + query_builder.push_bind(g); + query_builder.push(" or ios="); + query_builder.push_bind(g); + } + (None, None) => { + // if gd version isn't specifed, select whatever versions have the latest gd version + query_builder.push( + r#" WHERE + android=enum_last(NULL::gd_version) OR + win=enum_last(NULL::gd_version) OR + mac=enum_last(NULL::gd_version) OR + ios=enum_last(NULL::gd_version) + "#, + ); + } + } - if !accept_prereleases { - query_builder.push(" AND prerelease=FALSE "); - } + if !accept_prereleases { + query_builder.push(" AND prerelease=FALSE "); + } - query_builder.push(" ORDER BY "); + query_builder.push(" ORDER BY "); - if gd.is_none() { - if let Some(p) = platform { - // if there's a platform but no gd, order by the latest gd for that platform - match p { - VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => query_builder.push(" android"), - VerPlatform::Mac | VerPlatform::MacIntel | VerPlatform::MacArm => query_builder.push(" mac"), - VerPlatform::Win => query_builder.push(" win"), - VerPlatform::Ios => query_builder.push(" ios"), - // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), - }; - query_builder.push(" DESC, "); - } - } + if gd.is_none() { + if let Some(p) = platform { + // if there's a platform but no gd, order by the latest gd for that platform + match p { + VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => { + query_builder.push(" android") + } + VerPlatform::Mac | VerPlatform::MacIntel | VerPlatform::MacArm => { + query_builder.push(" mac") + } + VerPlatform::Win => query_builder.push(" win"), + VerPlatform::Ios => query_builder.push(" ios"), + // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), + }; + query_builder.push(" DESC, "); + } + } - query_builder.push(" created_at DESC LIMIT 1;"); + query_builder.push(" created_at DESC LIMIT 1;"); - match query_builder - .build_query_as::() - .fetch_optional(&mut *pool) - .await - { - Ok(Some(r)) => Ok(r.into_loader_version()), - Ok(None) => Err(ApiError::NotFound("".to_string())), - Err(e) => { - log::error!("{:?}", e); - Err(ApiError::DbError) - } - } - } + query_builder + .build_query_as::() + .fetch_optional(&mut *pool) + .await + .map_err(|e| e.into()) + .map(|x| x.map(|y| y.into_loader_version())) + } - pub async fn get_one(tag: &str, pool: &mut PgConnection) -> Result { - match sqlx::query_as!(LoaderVersionGetOne, - r#"SELECT - mac as "mac: _", win as "win: _", android as "android: _", ios as "ios: _", - tag, created_at, commit_hash, prerelease - FROM geode_versions - WHERE tag = $1"#, tag) - .fetch_optional(&mut *pool) - .await - { - Ok(Some(r)) => Ok(r.into_loader_version()), - Ok(None) => Err(ApiError::NotFound(format!("Loader version \"{tag}\" not found"))), - Err(e) => { - log::error!("{:?}", e); - Err(ApiError::DbError) - } - } - } + pub async fn get_one( + tag: &str, + pool: &mut PgConnection, + ) -> Result, DatabaseError> { + sqlx::query_as!( + LoaderVersionGetOne, + r#"SELECT + mac as "mac: _", win as "win: _", android as "android: _", ios as "ios: _", + tag, created_at, commit_hash, prerelease + FROM geode_versions + WHERE tag = $1"#, + tag + ) + .fetch_optional(&mut *pool) + .await + .map_err(|e| e.into()) + .map(|x| x.map(|y| y.into_loader_version())) + } - pub async fn create_version(version: LoaderVersionCreate, pool: &mut PgConnection) -> Result<(), ApiError> { - match sqlx::query!( - r#"INSERT INTO geode_versions - (tag, prerelease, mac, win, android, ios, commit_hash) - VALUES - ($1, $2, $3, $4, $5, $6, $7)"#, - version.tag, - version.prerelease, - version.mac as _, - version.win as _, - version.android as _, - version.ios as _, - version.commit_hash - ) - .execute(&mut *pool) - .await - { - Ok(_) => Ok(()), - Err(e) => { - log::error!("{:?}", e); - Err(ApiError::DbError) - } - } - } + pub async fn create_version( + version: LoaderVersionCreate, + pool: &mut PgConnection, + ) -> Result<(), DatabaseError> { + sqlx::query!( + r#"INSERT INTO geode_versions + (tag, prerelease, mac, win, android, ios, commit_hash) + VALUES + ($1, $2, $3, $4, $5, $6, $7)"#, + version.tag, + version.prerelease, + version.mac as _, + version.win as _, + version.android as _, + version.ios as _, + version.commit_hash + ) + .execute(&mut *pool) + .await + .map(|_| ()) + .map_err(|e| e.into()) + } - pub async fn get_many( - query: GetVersionsQuery, - per_page: i64, - page: i64, - pool: &mut PgConnection - ) -> Result, ApiError> { - let limit = per_page; - let offset = (page - 1) * per_page; + pub async fn get_many( + query: GetVersionsQuery, + per_page: i64, + page: i64, + pool: &mut PgConnection, + ) -> Result, DatabaseError> { + let limit = per_page; + let offset = (page - 1) * per_page; - let mut query_builder = QueryBuilder::new(r#" - SELECT mac, win, android, ios, tag, created_at, commit_hash, prerelease FROM geode_versions - "#); + let mut query_builder = QueryBuilder::new( + r#" + SELECT + mac, win, android, ios, tag, created_at, commit_hash, prerelease + FROM geode_versions + "#, + ); - match (query.platform, query.gd) { - (Some(p), Some(g)) => { - match p { - VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => query_builder.push(" WHERE android="), - VerPlatform::Mac | VerPlatform::MacIntel | VerPlatform::MacArm => query_builder.push(" WHERE mac="), - VerPlatform::Ios => query_builder.push(" WHERE ios="), - VerPlatform::Win => query_builder.push(" WHERE win="), - // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), - }; + match (query.platform, query.gd) { + (Some(p), Some(g)) => { + match p { + VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => { + query_builder.push(" WHERE android=") + } + VerPlatform::Mac | VerPlatform::MacIntel | VerPlatform::MacArm => { + query_builder.push(" WHERE mac=") + } + VerPlatform::Ios => query_builder.push(" WHERE ios="), + VerPlatform::Win => query_builder.push(" WHERE win="), + // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), + }; - query_builder.push_bind(g); - } - (Some(p), None) => { - match p { - VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => query_builder.push(" WHERE android IS NOT NULL"), - VerPlatform::Mac | VerPlatform::MacIntel | VerPlatform::MacArm => query_builder.push(" WHERE mac IS NOT NULL"), - VerPlatform::Ios => query_builder.push(" WHERE ios IS NOT NULL"), - VerPlatform::Win => query_builder.push(" WHERE win IS NOT NULL"), - // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), - }; - } - (None, Some(g)) => { - query_builder.push(" WHERE android="); - query_builder.push_bind(g); - query_builder.push(" or mac="); - query_builder.push_bind(g); - query_builder.push(" or win="); - query_builder.push_bind(g); - query_builder.push(" or ios="); - query_builder.push_bind(g); - } - _ => { - query_builder.push(" WHERE 1=1"); - } - } + query_builder.push_bind(g); + } + (Some(p), None) => { + match p { + VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => { + query_builder.push(" WHERE android IS NOT NULL") + } + VerPlatform::Mac | VerPlatform::MacIntel | VerPlatform::MacArm => { + query_builder.push(" WHERE mac IS NOT NULL") + } + VerPlatform::Ios => query_builder.push(" WHERE ios IS NOT NULL"), + VerPlatform::Win => query_builder.push(" WHERE win IS NOT NULL"), + // _ => return Err(ApiError::BadRequest("Invalid platform".to_string())), + }; + } + (None, Some(g)) => { + query_builder.push(" WHERE android="); + query_builder.push_bind(g); + query_builder.push(" or mac="); + query_builder.push_bind(g); + query_builder.push(" or win="); + query_builder.push_bind(g); + query_builder.push(" or ios="); + query_builder.push_bind(g); + } + _ => { + query_builder.push(" WHERE 1=1"); + } + } - if !query.prerelease { - query_builder.push(" AND prerelease=FALSE "); - } + if !query.prerelease { + query_builder.push(" AND prerelease=FALSE "); + } - query_builder.push(" ORDER BY created_at DESC "); + query_builder.push(" ORDER BY created_at DESC "); - query_builder.push(" LIMIT "); - query_builder.push_bind(limit); - query_builder.push(" OFFSET "); - query_builder.push_bind(offset); + query_builder.push(" LIMIT "); + query_builder.push_bind(limit); + query_builder.push(" OFFSET "); + query_builder.push_bind(offset); - match query_builder - .build_query_as::() - .fetch_all(&mut *pool) - .await - { - Ok(r) => - Ok(r.into_iter().map(|x| x.into_loader_version()).collect()), - Err(e) => { - log::error!("{:?}", e); - Err(ApiError::DbError) - } - } - } + query_builder + .build_query_as::() + .fetch_all(&mut *pool) + .await + .map(|x| x.into_iter().map(|y| y.into_loader_version()).collect()) + .map_err(|e| e.into()) + } } diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index e9ff820..e5fee15 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -6,14 +6,17 @@ use super::{ mod_link::ModLinks, tag::Tag, }; -use crate::database::repository::{developers, mods}; +use crate::{ + database::{repository::developers, DatabaseError}, + endpoints::ApiError, +}; use crate::{ endpoints::{ developers::{SimpleDevMod, SimpleDevModVersion}, mods::{IndexQueryParams, IndexSortType}, }, types::{ - api::{ApiError, PaginatedData}, + api::PaginatedData, models::{mod_version::ModVersion, mod_version_status::ModVersionStatusEnum}, }, }; @@ -24,7 +27,7 @@ use sqlx::{ types::chrono::{DateTime, Utc}, PgConnection, }; -use std::{collections::HashMap, str::FromStr}; +use std::collections::HashMap; #[derive(Serialize, Debug, sqlx::FromRow)] pub struct Mod { @@ -100,10 +103,10 @@ pub struct ModStats { } impl Mod { - pub async fn get_stats(pool: &mut PgConnection) -> Result { - match sqlx::query!( + pub async fn get_stats(pool: &mut PgConnection) -> Result { + let result = sqlx::query!( " - SELECT COUNT(id), SUM(download_count) + SELECT COUNT(id) as id_count, SUM(download_count) as download_sum FROM ( select m.id, m.download_count, row_number() over(partition by m.id) rn FROM mods m @@ -116,26 +119,20 @@ impl Mod { ) .fetch_optional(&mut *pool) .await + .inspect_err(|e| log::error!("failed to get mod stats: {}", e))?; + + if let Some((Some(total_count), Some(total_downloads))) = + result.map(|o| (o.id_count, o.download_sum)) { - Err(e) => { - log::error!("{}", e); - Err(ApiError::DbError) - } - Ok(r) => { - if let Some((Some(total_count), Some(total_downloads))) = - r.map(|o| (o.count, o.sum)) - { - Ok(ModStats { - total_count, - total_downloads, - }) - } else { - Ok(ModStats { - total_count: 0, - total_downloads: 0, - }) - } - } + Ok(ModStats { + total_count, + total_downloads, + }) + } else { + Ok(ModStats { + total_count: 0, + total_downloads: 0, + }) } } @@ -152,34 +149,10 @@ impl Mod { let limit = per_page; let offset = (page - 1) * per_page; - let platforms: Option> = query + let platforms = query .platforms - .map(|p| { - let split: Vec<&str> = p.split(',').collect(); - let mut ret: Vec = Vec::with_capacity(split.len()); - - for i in split { - let trimmed = i.trim(); - let platform = VerPlatform::from_str(trimmed).or(Err(ApiError::BadRequest( - format!("Invalid platform: {}", i), - )))?; - match platform { - VerPlatform::Android => { - ret.push(VerPlatform::Android32); - ret.push(VerPlatform::Android64); - } - VerPlatform::Mac => { - ret.push(VerPlatform::MacArm); - ret.push(VerPlatform::MacIntel); - } - _ => ret.push(platform), - } - } - - Ok(ret) - }) + .map(|p| VerPlatform::parse_query_string(&p)) .transpose()?; - let status = query.status.unwrap_or(ModVersionStatusEnum::Accepted); let developer = match query.developer { @@ -302,8 +275,7 @@ impl Mod { .bind(status) .fetch_all(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch mod index: {}", e)) - .or(Err(ApiError::DbError))?; + .inspect_err(|e| log::error!("Failed to fetch mod index: {}", e))?; let count: i64 = sqlx::query_scalar(&format!( "SELECT COUNT(DISTINCT m.id) @@ -326,8 +298,7 @@ impl Mod { .bind(status) .fetch_optional(&mut *pool) .await - .inspect_err(|e| log::error!("Failed to fetch mod index count: {}", e)) - .or(Err(ApiError::DbError))? + .inspect_err(|e| log::error!("Failed to fetch mod index count: {}", e))? .unwrap_or_default(); if records.is_empty() { @@ -342,7 +313,7 @@ impl Mod { } let ids: Vec = records.iter().map(|x| x.id.clone()).collect(); - let versions = ModVersion::get_latest_for_mods( + let mut versions = ModVersion::get_latest_for_mods( pool, &ids, query.gd, @@ -350,30 +321,29 @@ impl Mod { geode.as_ref(), ) .await?; - let developers = developers::get_all_for_mods(&ids, pool).await?; + let mut developers = developers::get_all_for_mods(&ids, pool).await?; let links = ModLinks::fetch_for_mods(&ids, pool).await?; - let mut mod_version_ids: Vec = vec![]; - for (_, mod_version) in versions.iter() { - mod_version_ids.push(mod_version.id); - } + let mod_version_ids: Vec = versions + .iter() + .map(|(_, mod_version)| mod_version.id) + .collect(); - let gd_versions = ModGDVersion::get_for_mod_versions(&mod_version_ids, pool).await?; - let tags = Tag::get_tags_for_mods(&ids, pool).await?; + let mut gd_versions = ModGDVersion::get_for_mod_versions(&mod_version_ids, pool).await?; + let mut tags = Tag::get_tags_for_mods(&ids, pool).await?; let ret = records .into_iter() .map(|x| { - let mut version = versions.get(&x.id).cloned().unwrap(); - let gd_ver = gd_versions.get(&version.id).cloned().unwrap_or_default(); - version.gd = gd_ver; + let mut version = versions.remove(&x.id).unwrap(); + version.gd = gd_versions.remove(&version.id).unwrap_or_default(); - let devs = developers.get(&x.id).cloned().unwrap_or_default(); - let tags = tags.get(&x.id).cloned().unwrap_or_default(); + let devs = developers.remove(&x.id).unwrap_or_default(); + let tags = tags.remove(&x.id).unwrap_or_default(); let links = links.iter().find(|link| link.mod_id == x.id).cloned(); Mod { - id: x.id.clone(), - repository: x.repository.clone(), + id: x.id, + repository: x.repository, download_count: x.download_count, featured: x.featured, versions: vec![version], @@ -446,7 +416,7 @@ impl Mod { status: ModVersionStatusEnum, only_owner: bool, pool: &mut PgConnection, - ) -> Result, ApiError> { + ) -> Result, DatabaseError> { struct Record { id: String, featured: bool, @@ -484,8 +454,7 @@ impl Mod { ) .fetch_all(&mut *pool) .await - .inspect_err(|x| log::error!("Failed to fetch developer mods: {}", x)) - .or(Err(ApiError::DbError))?; + .inspect_err(|x| log::error!("Failed to fetch developer mods: {}", x))?; if records.is_empty() { return Ok(vec![]); @@ -532,30 +501,33 @@ impl Mod { id: &str, only_accepted: bool, pool: &mut PgConnection, - ) -> Result, ApiError> { - let records: Vec = sqlx::query_as( + ) -> Result, DatabaseError> { + let records = sqlx::query_as!( + ModRecordGetOne, r#"SELECT m.id, m.repository, m.about, m.changelog, m.featured, m.download_count as mod_download_count, m.created_at, m.updated_at, mv.id as version_id, mv.name, mv.description, mv.version, mv.download_link, mv.download_count as mod_version_download_count, mv.created_at as mod_version_created_at, mv.updated_at as mod_version_updated_at, mv.hash, - format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as geode, - mv.early_load, mv.api, mv.mod_id, mvs.status, mvs.info + format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as "geode!: _", + mv.early_load, mv.api, mv.mod_id, mvs.status as "status: _", mvs.info FROM mods m INNER JOIN mod_versions mv ON m.id = mv.mod_id INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id WHERE m.id = $1 AND ($2 = false OR mvs.status = 'accepted') ORDER BY mv.id DESC"#, + id, + only_accepted ) - .bind(id) - .bind(only_accepted) - .fetch_all(&mut *pool) - .await - .or(Err(ApiError::DbError))?; + .fetch_all(&mut *pool) + .await + .inspect_err(|e| log::error!("{}", e))?; + if records.is_empty() { return Ok(None); } + let mut versions: Vec = records .iter() .map(|x| ModVersion { @@ -629,25 +601,18 @@ impl Mod { } /// At the moment this is only used to set the mod to featured. - /// Checks if the mod exists. + /// DOES NOT check if the mod exists pub async fn update_mod( id: &str, featured: bool, pool: &mut PgConnection, - ) -> Result<(), ApiError> { - if !mods::exists(id, &mut *pool).await? { - return Err(ApiError::NotFound(format!("Mod {} doesn't exist", id))); - } - + ) -> Result<(), DatabaseError> { sqlx::query!("UPDATE mods SET featured = $1 WHERE id = $2", featured, id) .execute(&mut *pool) .await - .map_err(|e| { - log::error!("Failed to update mod {}: {}", id, e); - ApiError::DbError - })?; - - Ok(()) + .inspect_err(|e| log::error!("Failed to update mod {id}: {e}")) + .map_err(|e| e.into()) + .map(|_| ()) } pub async fn get_updates( @@ -656,7 +621,7 @@ impl Mod { geode: &semver::Version, gd: GDVersionEnum, pool: &mut PgConnection, - ) -> Result, ApiError> { + ) -> Result, DatabaseError> { #[derive(sqlx::FromRow)] struct QueryResult { id: String, @@ -716,8 +681,7 @@ impl Mod { ) .fetch_all(&mut *pool) .await - .inspect_err(|x| log::error!("Failed to fetch mod updates: {}", x)) - .or(Err(ApiError::DbError))?; + .inspect_err(|x| log::error!("Failed to fetch mod updates: {}", x))?; if result.is_empty() { return Ok(vec![]); diff --git a/src/types/models/mod_gd_version.rs b/src/types/models/mod_gd_version.rs index de22f0d..025a68d 100644 --- a/src/types/models/mod_gd_version.rs +++ b/src/types/models/mod_gd_version.rs @@ -1,12 +1,9 @@ -use std::{ - collections::{hash_map::Entry, HashMap}, - str::FromStr, -}; +use std::{collections::HashMap, str::FromStr}; use serde::{Deserialize, Serialize}; -use sqlx::{PgConnection, Postgres, QueryBuilder}; +use sqlx::PgConnection; -use crate::types::{api::ApiError, mod_json::ModJson}; +use crate::{database::DatabaseError, types::mod_json::ModJson}; #[derive(sqlx::Type, Debug, Deserialize, Serialize, Clone, Copy)] #[sqlx(type_name = "gd_version")] @@ -66,6 +63,12 @@ impl FromStr for GDVersionEnum { } } +#[derive(thiserror::Error, Debug)] +pub enum PlatformParseError { + #[error("Invalid platform {0}")] + InvalidPlatform(String), +} + #[derive(sqlx::Type, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)] #[sqlx(type_name = "gd_ver_platform", rename_all = "lowercase")] #[serde(rename_all = "lowercase")] @@ -106,28 +109,28 @@ impl FromStr for VerPlatform { } impl VerPlatform { - pub fn parse_query_string(s: &str) -> Vec { + pub fn parse_query_string(s: &str) -> Result, PlatformParseError> { let mut ret = vec![]; - if s.is_empty() { - return ret; - } for x in s.split(',') { - match VerPlatform::from_str(x) { - Ok(v) => { - if v == VerPlatform::Android { - ret.push(VerPlatform::Android32); - ret.push(VerPlatform::Android64); - } else { - ret.push(v); - } + let x = x.trim(); + let v = VerPlatform::from_str(x) + .map_err(|_| PlatformParseError::InvalidPlatform(x.into()))?; + + match v { + VerPlatform::Android => { + ret.push(VerPlatform::Android32); + ret.push(VerPlatform::Android64); } - Err(_) => { - log::error!("invalid platform {}", x); + VerPlatform::Mac => { + ret.push(VerPlatform::MacArm); + ret.push(VerPlatform::MacIntel); } + default => ret.push(default), } } - ret + + Ok(ret) } } @@ -215,17 +218,21 @@ impl ModGDVersion { pub async fn get_for_mod_version( id: i32, pool: &mut PgConnection, - ) -> Result { - let result = sqlx::query_as!(ModGDVersion, r#"SELECT mgv.id, mgv.mod_id, mgv.gd AS "gd: _", mgv.platform as "platform: _" FROM mod_gd_versions mgv WHERE mgv.mod_id = $1"#, id) - .fetch_all(&mut *pool) - .await; - let result: Vec = match result { - Err(e) => { - log::info!("{:?}", e); - return Err(ApiError::DbError); - } - Ok(r) => r, - }; + ) -> Result { + let result = sqlx::query_as!( + ModGDVersion, + r#" + SELECT mgv.id, mgv.mod_id, mgv.gd AS "gd: _", mgv.platform as "platform: _" + FROM mod_gd_versions mgv + WHERE mgv.mod_id = $1 + "#, + id + ) + .fetch_all(&mut *pool) + .await + .inspect_err(|e| { + log::error!("Failed to fetch mod_gd_versions for mod_version {id}: {e}") + })?; let mut ret = DetailedGDVersion { win: None, mac: None, @@ -260,71 +267,43 @@ impl ModGDVersion { // hello pub async fn get_for_mod_versions( - versions: &Vec, + versions: &[i32], pool: &mut PgConnection, - ) -> Result, ApiError> { + ) -> Result, DatabaseError> { if versions.is_empty() { - return Err(ApiError::DbError); - } - let mut builder: QueryBuilder = - QueryBuilder::new("SELECT * FROM mod_gd_versions WHERE mod_id IN ("); - let mut separated = builder.separated(", "); - for i in versions { - separated.push_bind(i); + return Ok(HashMap::new()); } - separated.push_unseparated(")"); - let result = builder - .build_query_as::() - .fetch_all(&mut *pool) - .await; - let result = match result { - Err(e) => { - log::info!("{:?}", e); - return Err(ApiError::DbError); - } - Ok(r) => r, - }; + let result = sqlx::query_as!( + ModGDVersion, + r#"SELECT + id, mod_id, gd as "gd: _", platform as "platform: _" + FROM mod_gd_versions + WHERE mod_id = ANY($1)"#, + versions + ) + .fetch_all(&mut *pool) + .await + .inspect_err(|e| log::error!("Failed to fetch mod_gd_versions: {}", e))?; let mut ret: HashMap = HashMap::new(); for i in result { - match ret.entry(i.mod_id) { - Entry::Vacant(e) => { - let mut ver = DetailedGDVersion::default(); - match i.platform { - VerPlatform::Android => { - ver.android32 = Some(i.gd); - ver.android64 = Some(i.gd); - } - VerPlatform::Android32 => ver.android32 = Some(i.gd), - VerPlatform::Android64 => ver.android64 = Some(i.gd), - VerPlatform::MacArm => ver.mac_arm = Some(i.gd), - VerPlatform::MacIntel => ver.mac_intel = Some(i.gd), - VerPlatform::Mac => { - ver.mac_arm = Some(i.gd); - ver.mac_intel = Some(i.gd); - } - VerPlatform::Ios => ver.ios = Some(i.gd), - VerPlatform::Win => ver.win = Some(i.gd), - } - e.insert(ver); + let ver = ret.entry(i.mod_id).or_default(); + match i.platform { + VerPlatform::Android => { + ver.android32 = Some(i.gd); + ver.android64 = Some(i.gd); + } + VerPlatform::Android32 => ver.android32 = Some(i.gd), + VerPlatform::Android64 => ver.android64 = Some(i.gd), + VerPlatform::MacArm => ver.mac_arm = Some(i.gd), + VerPlatform::MacIntel => ver.mac_intel = Some(i.gd), + VerPlatform::Mac => { + ver.mac_arm = Some(i.gd); + ver.mac_intel = Some(i.gd); } - Entry::Occupied(mut e) => match i.platform { - VerPlatform::Android => { - e.get_mut().android32 = Some(i.gd); - e.get_mut().android64 = Some(i.gd); - } - VerPlatform::Android32 => e.get_mut().android32 = Some(i.gd), - VerPlatform::Android64 => e.get_mut().android64 = Some(i.gd), - VerPlatform::Mac => { - e.get_mut().mac_arm = Some(i.gd); - e.get_mut().mac_intel = Some(i.gd); - } - VerPlatform::MacArm => e.get_mut().mac_arm = Some(i.gd), - VerPlatform::MacIntel => e.get_mut().mac_intel = Some(i.gd), - VerPlatform::Ios => e.get_mut().ios = Some(i.gd), - VerPlatform::Win => e.get_mut().win = Some(i.gd), - }, + VerPlatform::Ios => ver.ios = Some(i.gd), + VerPlatform::Win => ver.win = Some(i.gd), } } diff --git a/src/types/models/mod_link.rs b/src/types/models/mod_link.rs index 0216d9c..de2feb8 100644 --- a/src/types/models/mod_link.rs +++ b/src/types/models/mod_link.rs @@ -1,7 +1,7 @@ use serde::Serialize; use sqlx::PgConnection; -use crate::types::api::ApiError; +use crate::database::DatabaseError; #[derive(Serialize, Debug, Clone)] pub struct ModLinks { @@ -16,8 +16,8 @@ impl ModLinks { pub async fn fetch( mod_id: &str, pool: &mut PgConnection, - ) -> Result, ApiError> { - match sqlx::query_as!( + ) -> Result, DatabaseError> { + sqlx::query_as!( ModLinks, "SELECT mod_id, community, homepage, source @@ -27,24 +27,19 @@ impl ModLinks { ) .fetch_optional(pool) .await - { - Ok(r) => Ok(r), - Err(e) => { - log::error!("Failed to fetch mod links for mod {}. Error: {}", mod_id, e); - Err(ApiError::DbError) - } - } + .inspect_err(|e| log::error!("Failed to fetch mod links for mod {}. Error: {}", mod_id, e)) + .map_err(|e| e.into()) } pub async fn fetch_for_mods( mod_ids: &Vec, pool: &mut PgConnection, - ) -> Result, ApiError> { + ) -> Result, DatabaseError> { if mod_ids.is_empty() { return Ok(vec![]); } - match sqlx::query_as!( + sqlx::query_as!( ModLinks, "SELECT mod_id, community, homepage, source @@ -54,12 +49,7 @@ impl ModLinks { ) .fetch_all(pool) .await - { - Err(e) => { - log::error!("Failed to fetch mod links for multiple mods. Error: {}", e); - Err(ApiError::DbError) - } - Ok(r) => Ok(r), - } + .inspect_err(|e| log::error!("Failed to fetch mod links for multiple mods. Error: {}", e)) + .map_err(|e| e.into()) } } diff --git a/src/types/models/mod_version.rs b/src/types/models/mod_version.rs index 3b0963d..1a06be4 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -1,13 +1,13 @@ -use std::collections::HashMap; - use crate::database::repository::developers; -use crate::types::api::{create_download_link, ApiError, PaginatedData}; +use crate::database::DatabaseError; +use crate::types::api::{create_download_link, PaginatedData}; use chrono::SecondsFormat; use serde::Serialize; use sqlx::{ types::chrono::{DateTime, Utc}, PgConnection, Postgres, QueryBuilder, }; +use std::collections::HashMap; use super::{ dependency::{Dependency, ModVersionCompare, ResponseDependency}, @@ -144,7 +144,7 @@ impl ModVersion { pub async fn get_index( query: IndexQuery, pool: &mut PgConnection, - ) -> Result, ApiError> { + ) -> Result, DatabaseError> { let limit = query.per_page; let offset = (query.page - 1) * query.per_page; @@ -161,7 +161,7 @@ impl ModVersion { ); let mut counter_q: QueryBuilder = QueryBuilder::new( r#" - SELECT COUNT(DISTINCT mv.id) + SELECT COUNT(DISTINCT mv.id) FROM mod_versions mv INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id INNER JOIN mod_gd_versions mgv ON mgv.mod_id = mv.id @@ -237,25 +237,17 @@ impl ModVersion { q.push(sql); q.push_bind(offset); - let records = match q + let records = q .build_query_as::() .fetch_all(&mut *pool) .await - { - Err(e) => { - log::error!("{}", e); - return Err(ApiError::DbError); - } - Ok(r) => r, - }; + .inspect_err(|e| log::error!("Failed to fetch index: {e}"))?; - let count: i64 = match counter_q.build_query_scalar().fetch_one(&mut *pool).await { - Err(e) => { - log::error!("{}", e); - return Err(ApiError::DbError); - } - Ok(c) => c, - }; + let count: i64 = counter_q + .build_query_scalar() + .fetch_one(&mut *pool) + .await + .inspect_err(|e| log::error!("Failed to fetch index count: {e}"))?; if records.is_empty() { return Ok(PaginatedData { @@ -305,16 +297,16 @@ impl ModVersion { gd: Option, platforms: Option<&[VerPlatform]>, geode: Option<&semver::Version>, - ) -> Result, ApiError> { + ) -> Result, DatabaseError> { if ids.is_empty() { return Ok(Default::default()); } let gd_vec = gd.map(|x| vec![GDVersionEnum::All, x]); - Ok(sqlx::query_as( + sqlx::query_as( "SELECT - q.name, q.id, q.description, q.version, + q.name, q.id, q.description, q.version, q.download_link, q.hash, q.geode, q.download_count, q.early_load, q.api, q.mod_id, 'accepted'::mod_version_status as status, @@ -369,54 +361,46 @@ impl ModVersion { .fetch_all(&mut *pool) .await .inspect_err(|x| log::error!("Failed to fetch latest versions for mods: {}", x)) - .or(Err(ApiError::DbError))? - .into_iter() - .map(|i: ModVersionGetOne| (i.mod_id.clone(), i.into_mod_version())) - .collect::>()) + .map_err(|e| e.into()) + .map(|result: Vec| { + result.into_iter() + .map(|i| (i.mod_id.clone(), i.into_mod_version())) + .collect::>() + }) } pub async fn get_pending_for_mods( - ids: &Vec, + ids: &[String], pool: &mut PgConnection, - ) -> Result>, ApiError> { + ) -> Result>, DatabaseError> { if ids.is_empty() { - return Ok(Default::default()); + return Ok(HashMap::new()); } - let mut query_builder: QueryBuilder = QueryBuilder::new( + let records = sqlx::query_as!( + ModVersionGetOne, r#"SELECT DISTINCT - mv.name, mv.id, mv.description, mv.version, mv.download_link, mv.hash, - format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as geode, - mv.download_count, - mv.early_load, mv.api, mv.mod_id, mv.created_at, mv.updated_at, mvs.status FROM mod_versions mv + mv.name, mv.id, mv.description, mv.version, mv.download_link, mv.hash, + format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as "geode!: _", + mv.download_count, mv.early_load, mv.api, mv.mod_id, mv.created_at, mv.updated_at, + 'pending'::mod_version_status as "status!: _", NULL as info + FROM mod_versions mv INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id - WHERE mvs.status = 'pending' AND mv.mod_id IN ("#, - ); - let mut separated = query_builder.separated(","); - - for id in ids { - separated.push_bind(id); - } - separated.push_unseparated(")"); - let records = query_builder - .build_query_as::() - .fetch_all(&mut *pool) - .await; - let records = match records { - Err(e) => { - log::info!("{:?}", e); - return Err(ApiError::DbError); - } - Ok(r) => r, - }; + WHERE mvs.status = 'pending' + AND mv.mod_id = ANY($1) + ORDER BY mv.id DESC"#, + ids + ).fetch_all(&mut *pool) + .await + .inspect_err(|e| log::error!("Failed to fetch pending mod versions: {}", e))?; let mut ret: HashMap> = HashMap::new(); for x in records.into_iter() { - let mod_id = x.mod_id.clone(); + let entry = ret.entry(x.mod_id.clone()).or_default(); let version = x.into_mod_version(); - ret.entry(mod_id).or_default().push(version); + entry.push(version); } Ok(ret) } @@ -427,20 +411,20 @@ impl ModVersion { platforms: Vec, major: Option, pool: &mut PgConnection, - ) -> Result { + ) -> Result, DatabaseError> { let mut query_builder: QueryBuilder = QueryBuilder::new( - r#"SELECT q.name, q.id, q.description, q.version, q.download_link, + r#"SELECT q.name, q.id, q.description, q.version, q.download_link, q.hash, q.geode, q.download_count, q.early_load, q.api, q.mod_id, q.status, q.created_at, q.updated_at FROM ( - SELECT mv.name, mv.id, mv.description, mv.version, mv.download_link, + SELECT mv.name, mv.id, mv.description, mv.version, mv.download_link, mv.hash, format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as geode, mv.download_count, mvs.status, mv.early_load, mv.api, mv.mod_id, mv.created_at, mv.updated_at, - row_number() over (partition by m.id order by mv.id desc) rn - FROM mods m + row_number() over (partition by m.id order by mv.id desc) rn + FROM mods m INNER JOIN mod_versions mv ON m.id = mv.mod_id INNER JOIN mod_gd_versions mgv ON mgv.mod_id = mv.id INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id @@ -472,19 +456,16 @@ impl ModVersion { query_builder.push(" AND mv.mod_id = "); query_builder.push_bind(id); query_builder.push(") q WHERE q.rn = 1"); - let mut version = match query_builder + + let version = query_builder .build_query_as::() .fetch_optional(&mut *pool) .await - { - Ok(Some(r)) => r.into_mod_version(), - Ok(None) => { - return Err(ApiError::NotFound("".to_string())); - } - Err(e) => { - log::error!("{:?}", e); - return Err(ApiError::DbError); - } + .inspect_err(|e| log::error!("Failed to fetch latest mod_version for mod {id}: {e}"))? + .map(|v| v.into_mod_version()); + + let Some(mut version) = version else { + return Ok(None); }; let ids: Vec = vec![version.id]; @@ -509,7 +490,7 @@ impl ModVersion { version.developers = Some(developers::get_all_for_mod(&version.mod_id, pool).await?); version.tags = Some(Tag::get_tags_for_mod(&version.mod_id, pool).await?); - Ok(version) + Ok(Some(version)) } pub async fn get_one( @@ -518,10 +499,10 @@ impl ModVersion { fetch_extras: bool, fetch_only_accepted: bool, pool: &mut PgConnection, - ) -> Result { - let result = match sqlx::query_as!( + ) -> Result, DatabaseError> { + let result = sqlx::query_as!( ModVersionGetOne, - r#"SELECT mv.id, mv.name, mv.description, mv.version, + r#"SELECT mv.id, mv.name, mv.description, mv.version, mv.download_link, mv.download_count, mv.hash, format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as "geode!: _", @@ -530,8 +511,8 @@ impl ModVersion { mv.mod_id, mvs.status as "status: _", mvs.info FROM mod_versions mv INNER JOIN mods m ON m.id = mv.mod_id - INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id - WHERE mv.mod_id = $1 AND mv.version = $2 + INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id + WHERE mv.mod_id = $1 AND mv.version = $2 AND (mvs.status = 'accepted' OR $3 = false)"#, id, version, @@ -539,16 +520,13 @@ impl ModVersion { ) .fetch_optional(&mut *pool) .await - { - Err(e) => { - log::error!("{}", e); - return Err(ApiError::DbError); - } - Ok(None) => return Err(ApiError::NotFound("Not found".to_string())), - Ok(Some(r)) => r, + .inspect_err(|e| log::error!("ModVersion::get_one failed: {e}"))? + .map(|x| x.into_mod_version()); + + let Some(mut version) = result else { + return Ok(None); }; - let mut version = result.into_mod_version(); if fetch_extras { version.gd = ModGDVersion::get_for_mod_version(version.id, pool).await?; let ids = vec![version.id]; @@ -569,14 +547,14 @@ impl ModVersion { version.tags = Some(Tag::get_tags_for_mod(&version.mod_id, pool).await?); } - Ok(version) + Ok(Some(version)) } pub async fn get_accepted_count( mod_id: &str, pool: &mut PgConnection, - ) -> Result { - let count = match sqlx::query_scalar!( + ) -> Result { + sqlx::query_scalar!( "SELECT COUNT(*) FROM mod_versions mv INNER JOIN mod_version_statuses mvs ON mv.status_id = mvs.id @@ -586,15 +564,7 @@ impl ModVersion { ) .fetch_one(&mut *pool) .await - { - Ok(Some(count)) => count, - Ok(None) => 0, - Err(e) => { - log::error!("{}", e); - return Err(ApiError::DbError); - } - }; - - Ok(count) + .map(|x| x.unwrap_or_default()) + .map_err(|e| e.into()) } } diff --git a/src/types/models/stats.rs b/src/types/models/stats.rs index c7e82b3..2cf4b0b 100644 --- a/src/types/models/stats.rs +++ b/src/types/models/stats.rs @@ -1,5 +1,4 @@ -use crate::database::repository::developers; -use crate::types::api::ApiError; +use crate::{database::repository::developers, endpoints::ApiError}; use chrono::Utc; use reqwest::{header::HeaderValue, Client}; use serde::{Deserialize, Serialize}; @@ -34,7 +33,7 @@ impl Stats { Ok(Stats { total_mod_count: mod_stats.total_count, total_mod_downloads: mod_stats.total_downloads, - total_registered_developers: developers::index_count("", &mut *pool).await?, + total_registered_developers: developers::index_count(None, &mut *pool).await?, total_geode_downloads: Self::get_latest_github_release_download_count(&mut *pool) .await?, }) @@ -45,11 +44,9 @@ impl Stats { ) -> Result { // If release stats were fetched less than a day ago, just use cached stats if let Ok((cache_time, total_download_count)) = sqlx::query!( - " - SELECT s.checked_at, s.total_download_count + "SELECT s.checked_at, s.total_download_count FROM github_loader_release_stats s - ORDER BY s.checked_at DESC - " + ORDER BY s.checked_at DESC" ) .fetch_one(&mut *pool) .await @@ -63,21 +60,17 @@ impl Stats { // Fetch latest stats let new = Self::fetch_github_release_stats().await?; sqlx::query!( - " - INSERT INTO github_loader_release_stats (total_download_count, latest_loader_version) - VALUES ($1, $2) - ", + "INSERT INTO github_loader_release_stats (total_download_count, latest_loader_version) + VALUES ($1, $2)", new.0, new.1 ) .execute(&mut *pool) .await - .map_err(|e| { - log::error!("{}", e); - ApiError::DbError - })?; + .inspect_err(|e| log::error!("{}", e))?; Ok(new.0) } + async fn fetch_github_release_stats() -> Result<(i64, String), ApiError> { let client = Client::new(); let resp = client @@ -87,21 +80,23 @@ impl Stats { .query(&[("per_page", "100")]) .send() .await - .map_err(|e| { - log::info!("{}", e); - ApiError::InternalError + .inspect_err(|e| { + log::error!("Failed to request Geode release stats from GitHub: {}", e) })?; + if !resp.status().is_success() { - return Err(ApiError::InternalError); + return Err(ApiError::InternalError( + "Failed to retrieve Geode release stats from GitHub".into(), + )); } - let releases: Vec = resp.json().await.map_err(|e| { - log::info!("{}", e); - ApiError::InternalError - })?; + + let releases: Vec = resp.json().await?; let latest_release_tag = releases .iter() .find(|r| r.tag_name != "nightly") - .ok_or(ApiError::InternalError)? + .ok_or(ApiError::InternalError( + "No latest version detected on Geode repository".into(), + ))? .tag_name .clone(); Ok(( diff --git a/src/types/models/tag.rs b/src/types/models/tag.rs index 9c915f6..cf221f2 100644 --- a/src/types/models/tag.rs +++ b/src/types/models/tag.rs @@ -1,15 +1,9 @@ use std::collections::HashMap; +use crate::database::repository::mod_tags; +use crate::{database::DatabaseError, endpoints::ApiError}; use sqlx::PgConnection; -use crate::types::api::ApiError; - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct FetchedTag { - pub id: i32, - pub name: String, -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Tag { pub id: i32, @@ -22,8 +16,8 @@ impl Tag { pub async fn get_tags_for_mod( mod_id: &str, pool: &mut PgConnection, - ) -> Result, ApiError> { - let tags = match sqlx::query!( + ) -> Result, DatabaseError> { + sqlx::query!( "SELECT mod_tags.name FROM mod_tags INNER JOIN mods_mod_tags ON mod_tags.id = mods_mod_tags.tag_id WHERE mods_mod_tags.mod_id = $1", @@ -31,22 +25,16 @@ impl Tag { ) .fetch_all(&mut *pool) .await - { - Ok(tags) => tags, - Err(e) => { - log::error!("{}", e); - return Err(ApiError::DbError); - } - }; - - Ok(tags.iter().map(|t| t.name.clone()).collect()) + .inspect_err(|e| log::error!("{}", e)) + .map(|tags| tags.into_iter().map(|t| t.name).collect::>()) + .map_err(|e| e.into()) } pub async fn get_tags_for_mods( ids: &Vec, pool: &mut PgConnection, - ) -> Result>, ApiError> { - let tags = match sqlx::query!( + ) -> Result>, DatabaseError> { + let tags = sqlx::query!( "SELECT mod_tags.name, mods_mod_tags.mod_id FROM mod_tags INNER JOIN mods_mod_tags ON mod_tags.id = mods_mod_tags.tag_id WHERE mods_mod_tags.mod_id = ANY($1)", @@ -54,13 +42,7 @@ impl Tag { ) .fetch_all(&mut *pool) .await - { - Ok(tags) => tags, - Err(e) => { - log::error!("{}", e); - return Err(ApiError::DbError); - } - }; + .inspect_err(|e| log::error!("{}", e))?; let mut ret: HashMap> = HashMap::new(); for tag in tags { @@ -80,19 +62,13 @@ impl Tag { .map(|t| t.trim().to_lowercase()) .collect::>(); - let fetched = match sqlx::query!( + let fetched = sqlx::query!( "SELECT DISTINCT id, name FROM mod_tags WHERE name = ANY($1)", &tags ) .fetch_all(&mut *pool) .await - { - Ok(fetched) => fetched, - Err(e) => { - log::error!("{}", e); - return Err(ApiError::DbError); - } - }; + .inspect_err(|e| log::error!("Failed to fetch tags: {}", e))?; let fetched_ids = fetched.iter().map(|t| t.id).collect::>(); let fetched_names = fetched @@ -114,3 +90,35 @@ impl Tag { Ok(fetched_ids) } } + +pub async fn parse_tag_list( + tags: &[String], + mod_id: &str, + conn: &mut PgConnection, +) -> Result, ApiError> { + if tags.is_empty() { + return Ok(vec![]); + } + + let db_tags = mod_tags::get_allowed_for_mod(mod_id, &mut *conn).await?; + + let mut ret = Vec::with_capacity(tags.len()); + for tag in tags { + if let Some(t) = db_tags.iter().find(|t| t.name == *tag) { + ret.push(t.clone()); + } else { + let taglist = db_tags + .into_iter() + .map(|t| t.name) + .collect::>() + .join(", "); + + return Err(ApiError::BadRequest(format!( + "Tag '{}' isn't allowed. Only the following are allowed: '{}'", + tag, taglist + ))); + } + } + + Ok(ret) +}