diff --git a/.env b/.env index fefed0649..fe98ea8b8 100644 --- a/.env +++ b/.env @@ -40,4 +40,6 @@ GITHUB_CLIENT_SECRET=none RATE_LIMIT_IGNORE_IPS='["127.0.0.1"]' -WHITELISTED_MODPACK_DOMAINS='["cdn.modrinth.com", "edge.forgecdn.net", "github.com", "raw.githubusercontent.com"]' \ No newline at end of file +WHITELISTED_MODPACK_DOMAINS='["cdn.modrinth.com", "edge.forgecdn.net", "github.com", "raw.githubusercontent.com"]' + +ALLOWED_CALLBACK_URLS='["localhost", ".modrinth.com", "modrinth.com", "-modrinth.vercel.app"]' \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9ee8335aa..454fb5905 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1441,7 +1441,7 @@ dependencies = [ [[package]] name = "labrinth" -version = "2.4.1" +version = "2.4.2" dependencies = [ "actix", "actix-cors", diff --git a/Cargo.toml b/Cargo.toml index 0e04082ee..0df71d348 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "labrinth" -version = "2.4.1" +version = "2.4.2" #Team members, please add your emails and usernames authors = ["geometrically ", "Redblueflame ", "Aeledfyr ", "Charalampos Fanoulis ", "AppleTheGolden "] edition = "2018" diff --git a/sqlx-data.json b/sqlx-data.json index f05e23874..fc006d2d4 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -662,6 +662,32 @@ "nullable": [] } }, + "2278a7db5eb0474576fa9c86ba97bd6bf13864b3f9ce55ed2ab0cb94edbadaf5": { + "query": "\n SELECT url, expires FROM states\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "expires", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false + ] + } + }, "232d7d0319c20dd5fff29331b067d6c6373bcff761a77958a2bb5f59068a83a5": { "query": "\n UPDATE team_members\n SET permissions = $1\n WHERE (team_id = $2 AND user_id = $3)\n ", "describe": { @@ -772,50 +798,6 @@ "nullable": [] } }, - "2c7c46497580e96c2ede1a696c960a8f53af9b8d0fc995484618b9090add8890": { - "query": "\n SELECT id, title, notification_id, action_route, action_route_method\n FROM notifications_actions\n WHERE notification_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "notification_id", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "action_route", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "action_route_method", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false - ] - } - }, "2d2e5b06be5125226ed9e4d7b7b5f99043db73537f2199f2146bdcd56091ae75": { "query": "\n INSERT INTO team_members (id, team_id, user_id, role, permissions, accepted)\n VALUES ($1, $2, $3, $4, $5, $6)\n ", "describe": { @@ -858,6 +840,68 @@ "nullable": [] } }, + "35b728453ade9cd9c535411fff194105c05726cea29446c3532aec9bfa4ffd2d": { + "query": "\n SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n STRING_AGG(DISTINCT na.id || ' |||| ' || na.title || ' |||| ' || na.action_route || ' |||| ' || na.action_route_method, ' ~~~~ ') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = $1\n GROUP BY n.id, n.user_id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "read", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "notification_type", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "actions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + null + ] + } + }, "371048e45dd74c855b84cdb8a6a565ccbef5ad166ec9511ab20621c336446da6": { "query": "\n UPDATE mods\n SET follows = follows - 1\n WHERE id = $1\n ", "describe": { @@ -870,6 +914,74 @@ "nullable": [] } }, + "375d3e7221ca352efc3bec374b9924c864c1ea5808e99ee6b89e4dcb0d39ba7a": { + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n STRING_AGG(DISTINCT na.id || ' |||| ' || na.title || ' |||| ' || na.action_route || ' |||| ' || na.action_route_method, ' ~~~~ ') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = ANY($1)\n GROUP BY n.id, n.user_id\n ORDER BY n.created DESC;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "read", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "notification_type", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "actions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + null + ] + } + }, "3831c1b321e47690f1f54597506a0d43362eda9540c56acb19c06532bba50b01": { "query": "\n SELECT id, user_id, role, permissions, accepted\n FROM team_members\n WHERE team_id = $1\n ", "describe": { @@ -1313,116 +1425,6 @@ "nullable": [] } }, - "4e40451ec96cf4fa3a806fc71ffa953c087457e3778c25c30dcd664b04e496c6": { - "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured,\n STRING_AGG(DISTINCT gv.version, ' ~~~~ ') game_versions, STRING_AGG(DISTINCT l.loader, ' ~~~~ ') loaders,\n STRING_AGG(DISTINCT f.id || ' |||| ' || f.is_primary || ' |||| ' || f.size || ' |||| ' || f.url || ' |||| ' || f.filename, ' ~~~~ ') files,\n STRING_AGG(DISTINCT h.algorithm || ' |||| ' || encode(h.hash, 'escape') || ' |||| ' || h.file_id, ' ~~~~ ') hashes,\n STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ' |||| ' || COALESCE(d.mod_dependency_id, 0) || ' |||| ' || d.dependency_type || ' |||| ' || COALESCE(d.dependency_file_name, ' '), ' ~~~~ ') dependencies\n FROM versions v\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = ANY($1)\n GROUP BY v.id\n ORDER BY v.date_published ASC;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "author_id", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "version_name", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "changelog", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "changelog_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 9, - "name": "version_type", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "featured", - "type_info": "Bool" - }, - { - "ordinal": 11, - "name": "game_versions", - "type_info": "Text" - }, - { - "ordinal": 12, - "name": "loaders", - "type_info": "Text" - }, - { - "ordinal": 13, - "name": "files", - "type_info": "Text" - }, - { - "ordinal": 14, - "name": "hashes", - "type_info": "Text" - }, - { - "ordinal": 15, - "name": "dependencies", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - null, - null, - null, - null, - null - ] - } - }, "4e9f9eafbfd705dfc94571018cb747245a98ea61bad3fae4b3ce284229d99955": { "query": "\n UPDATE mods\n SET description = $1\n WHERE (id = $2)\n ", "describe": { @@ -1714,6 +1716,34 @@ ] } }, + "57ff857e0d7f6deab7da6e806b83c61648809c8820cf1a46e833ff97583fe888": { + "query": "\n SELECT DISTINCT ON(v.date_published, v.id) version_id, v.date_published FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[]))\n WHERE v.mod_id = $1\n ORDER BY v.date_published, v.id ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "date_published", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8", + "VarcharArray", + "VarcharArray" + ] + }, + "nullable": [ + false, + false + ] + } + }, "5917ab5017e27be2c4c5231426b19c3b37fd171ff47f97a0cb4e2094a0234298": { "query": "\n SELECT id, name FROM project_types\n WHERE name = ANY($1)\n ", "describe": { @@ -1936,28 +1966,6 @@ "nullable": [] } }, - "5ff8fd471ff62f86aa95e52cee2723b31ec3d7fc53c3ef1454df40eef0ceff53": { - "query": "\n SELECT version.id FROM (\n SELECT DISTINCT ON(v.id) v.id, v.date_published FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[]))\n WHERE v.mod_id = $1\n ) AS version\n ORDER BY version.date_published ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "VarcharArray", - "VarcharArray" - ] - }, - "nullable": [ - false - ] - } - }, "6131d32a65f5e04775308386812f25c6d8464582678536a392a4a3737667f363": { "query": "\n SELECT id, short, name FROM licenses\n ", "describe": { @@ -2022,116 +2030,6 @@ ] } }, - "62ebb6f33a26f05f28b3d175ef19cb247c7c7f1ac0fce2fae15e6787b33a1b96": { - "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured,\n STRING_AGG(DISTINCT gv.version, ' ~~~~ ') game_versions, STRING_AGG(DISTINCT l.loader, ' ~~~~ ') loaders,\n STRING_AGG(DISTINCT f.id || ' |||| ' || f.is_primary || ' |||| ' || f.size || ' |||| ' || f.url || ' |||| ' || f.filename, ' ~~~~ ') files,\n STRING_AGG(DISTINCT h.algorithm || ' |||| ' || encode(h.hash, 'escape') || ' |||| ' || h.file_id, ' ~~~~ ') hashes,\n STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ' |||| ' || COALESCE(d.mod_dependency_id, 0) || ' |||| ' || d.dependency_type || ' |||| ' || COALESCE(d.dependency_file_name, ' '), ' ~~~~ ') dependencies\n FROM versions v\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = $1\n GROUP BY v.id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "author_id", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "version_name", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "changelog", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "changelog_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 9, - "name": "version_type", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "featured", - "type_info": "Bool" - }, - { - "ordinal": 11, - "name": "game_versions", - "type_info": "Text" - }, - { - "ordinal": 12, - "name": "loaders", - "type_info": "Text" - }, - { - "ordinal": 13, - "name": "files", - "type_info": "Text" - }, - { - "ordinal": 14, - "name": "hashes", - "type_info": "Text" - }, - { - "ordinal": 15, - "name": "dependencies", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - null, - null, - null, - null, - null - ] - } - }, "67d021f0776276081d3c50ca97afa6b78b98860bf929009e845e9c00a192e3b5": { "query": "\n SELECT id FROM report_types\n WHERE name = $1\n ", "describe": { @@ -2999,26 +2897,6 @@ "nullable": [] } }, - "7be4ba7c3dd53abd79715b9a9ead6b8815a2e4994f6887ac853f832c5ca17150": { - "query": "\n SELECT id\n FROM notifications\n WHERE user_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "7c61fee015231f0a97c25d24f2c6be24821e39e330ab82344ad3b985d0d2aaea": { "query": "\n SELECT id FROM mods_gallery\n WHERE image_url = $1\n ", "describe": { @@ -3427,6 +3305,116 @@ ] } }, + "8c65c87d288ca385eee56f98a0880eac5bf73d2760af70b7d660ded5f7a619f2": { + "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured,\n STRING_AGG(DISTINCT gv.version || ' |||| ' || gv.created, ' ~~~~ ') game_versions, STRING_AGG(DISTINCT l.loader, ' ~~~~ ') loaders,\n STRING_AGG(DISTINCT f.id || ' |||| ' || f.is_primary || ' |||| ' || f.size || ' |||| ' || f.url || ' |||| ' || f.filename, ' ~~~~ ') files,\n STRING_AGG(DISTINCT h.algorithm || ' |||| ' || encode(h.hash, 'escape') || ' |||| ' || h.file_id, ' ~~~~ ') hashes,\n STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ' |||| ' || COALESCE(d.mod_dependency_id, 0) || ' |||| ' || d.dependency_type || ' |||| ' || COALESCE(d.dependency_file_name, ' '), ' ~~~~ ') dependencies\n FROM versions v\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = ANY($1)\n GROUP BY v.id\n ORDER BY v.date_published ASC;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "version_name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "changelog", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "version_type", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "featured", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "game_versions", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "loaders", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "files", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "hashes", + "type_info": "Text" + }, + { + "ordinal": 15, + "name": "dependencies", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + null, + null, + null, + null, + null + ] + } + }, "8f706d78ac4235ea04c59e2c220a4791e1d08fdf287b783b4aaef36fd2445467": { "query": "\n DELETE FROM loaders\n WHERE loader = $1\n ", "describe": { @@ -3697,6 +3685,116 @@ "nullable": [] } }, + "a007ddf78c7bcbac5e511d15d0f130ecb5527fefcdce02972ede5b5cd2b10630": { + "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured,\n STRING_AGG(DISTINCT gv.version || ' |||| ' || gv.created, ' ~~~~ ') game_versions, STRING_AGG(DISTINCT l.loader, ' ~~~~ ') loaders,\n STRING_AGG(DISTINCT f.id || ' |||| ' || f.is_primary || ' |||| ' || f.size || ' |||| ' || f.url || ' |||| ' || f.filename, ' ~~~~ ') files,\n STRING_AGG(DISTINCT h.algorithm || ' |||| ' || encode(h.hash, 'escape') || ' |||| ' || h.file_id, ' ~~~~ ') hashes,\n STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ' |||| ' || COALESCE(d.mod_dependency_id, 0) || ' |||| ' || d.dependency_type || ' |||| ' || COALESCE(d.dependency_file_name, ' '), ' ~~~~ ') dependencies\n FROM versions v\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id = $1\n GROUP BY v.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "version_name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "changelog", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "version_type", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "featured", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "game_versions", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "loaders", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "files", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "hashes", + "type_info": "Text" + }, + { + "ordinal": 15, + "name": "dependencies", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + null, + null, + null, + null, + null + ] + } + }, "a39ce28b656032f862b205cffa393a76b989f4803654a615477a94fda5f57354": { "query": "\n DELETE FROM states\n WHERE id = $1\n ", "describe": { @@ -4719,6 +4817,74 @@ ] } }, + "c83cef31a25dd3037fa6049da572301e3e85871dcc3a1de8acb395d34fa0cf74": { + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n STRING_AGG(DISTINCT na.id || ' |||| ' || na.title || ' |||| ' || na.action_route || ' |||| ' || na.action_route_method, ' ~~~~ ') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.user_id = $1\n GROUP BY n.id, n.user_id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "read", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "notification_type", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "actions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + null + ] + } + }, "c9d63ed46799db7c30a7e917d97a5d4b2b78b0234cce49e136fa57526b38c1ca": { "query": "\n SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)\n ", "describe": { @@ -5681,62 +5847,6 @@ ] } }, - "ea96ab7c1290f4caddcef8ecf2aec0216654faca05ff760ffa553ad3e32827f5": { - "query": "\n SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type\n FROM notifications n\n WHERE n.id = $1\n GROUP BY n.id, n.user_id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "text", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "link", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "created", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "read", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "notification_type", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true - ] - } - }, "ebef881a0dae70e990814e567ed3de9565bb29b772782bc974c953af195fd6d7": { "query": "\n SELECT n.id FROM notifications n\n WHERE n.user_id = $1\n ", "describe": { @@ -5957,32 +6067,6 @@ "nullable": [] } }, - "f7bea04e8e279e27a24de1bdf3c413daa8677994df5131494b28691ed6611efc": { - "query": "\n SELECT url,expires FROM states\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "url", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "expires", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false - ] - } - }, "f8c00875a7450c74423f9913cc3500898e9fcb6aa7eb8fc2f6fd16dc560773de": { "query": "\n SELECT short, name FROM donation_platforms\n WHERE id = $1\n ", "describe": { diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index e6a1dc3bd..f7d4d9556 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -2,6 +2,7 @@ // TODO: remove attr once routes are created use thiserror::Error; +use time::OffsetDateTime; pub mod categories; pub mod ids; @@ -125,3 +126,11 @@ impl ids::ProjectTypeId { Ok(result.map(|r| ids::ProjectTypeId(r.id))) } } + +pub fn convert_postgres_date(input: &str) -> OffsetDateTime { + OffsetDateTime::parse( + format!("{}:00Z", input.replace(' ', "T")), + time::Format::Rfc3339, + ) + .unwrap_or_else(|_| OffsetDateTime::now_utc()) +} diff --git a/src/database/models/notification_item.rs b/src/database/models/notification_item.rs index c75c0cbac..bdef52f71 100644 --- a/src/database/models/notification_item.rs +++ b/src/database/models/notification_item.rs @@ -118,31 +118,40 @@ impl Notification { id: NotificationId, executor: E, ) -> Result, sqlx::error::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let (notifications, actions) = futures::join!( - sqlx::query!( - " - SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type - FROM notifications n - WHERE n.id = $1 - GROUP BY n.id, n.user_id; - ", - id as NotificationId, - ) - .fetch_optional(executor), - sqlx::query!( - " - SELECT id, title, notification_id, action_route, action_route_method - FROM notifications_actions - WHERE notification_id = $1 - ", - id as NotificationId, - ).fetch_all(executor), - ); + let result = sqlx::query!( + " + SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, + STRING_AGG(DISTINCT na.id || ' |||| ' || na.title || ' |||| ' || na.action_route || ' |||| ' || na.action_route_method, ' ~~~~ ') actions + FROM notifications n + LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id + WHERE n.id = $1 + GROUP BY n.id, n.user_id; + ", + id as NotificationId, + ) + .fetch_optional(executor) + .await?; + + if let Some(row) = result { + let mut actions: Vec = Vec::new(); + + row.actions.unwrap_or_default().split(" ~~~~ ").for_each(|x| { + let action: Vec<&str> = x.split(" |||| ").collect(); + + if action.len() >= 3 { + actions.push(NotificationAction { + id: NotificationActionId(action[0].parse().unwrap_or(0)), + notification_id: id, + title: action[1].to_string(), + action_route_method: action[3].to_string(), + action_route: action[2].to_string(), + }); + } + }); - if let Some(row) = notifications? { Ok(Some(Notification { id, user_id: UserId(row.user_id), @@ -152,16 +161,7 @@ impl Notification { link: row.link, read: row.read, created: row.created, - actions: actions? - .into_iter() - .map(|x| NotificationAction { - id: NotificationActionId(x.id), - notification_id: NotificationId(x.notification_id), - title: x.title, - action_route_method: x.action_route_method, - action_route: x.action_route, - }) - .collect(), + actions, })) } else { Ok(None) @@ -172,38 +172,116 @@ impl Notification { notification_ids: Vec, exec: E, ) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - futures::future::try_join_all( - notification_ids.into_iter().map(|id| Self::get(id, exec)), + use futures::stream::TryStreamExt; + + let notification_ids_parsed: Vec = notification_ids.into_iter().map(|x| x.0).collect(); + sqlx::query!( + " + SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, + STRING_AGG(DISTINCT na.id || ' |||| ' || na.title || ' |||| ' || na.action_route || ' |||| ' || na.action_route_method, ' ~~~~ ') actions + FROM notifications n + LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id + WHERE n.id = ANY($1) + GROUP BY n.id, n.user_id + ORDER BY n.created DESC; + ", + ¬ification_ids_parsed ) - .await - .map(|x| x.into_iter().flatten().collect()) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|row| { + let id = NotificationId(row.id); + let mut actions: Vec = Vec::new(); + + row.actions.unwrap_or_default().split(" ~~~~ ").for_each(|x| { + let action: Vec<&str> = x.split(" |||| ").collect(); + + if action.len() >= 3 { + actions.push(NotificationAction { + id: NotificationActionId(action[0].parse().unwrap_or(0)), + notification_id: id, + title: action[1].to_string(), + action_route_method: action[3].to_string(), + action_route: action[2].to_string(), + }); + } + }); + + Notification { + id, + user_id: UserId(row.user_id), + notification_type: row.notification_type, + title: row.title, + text: row.text, + link: row.link, + read: row.read, + created: row.created, + actions, + } + })) + }) + .try_collect::>() + .await } pub async fn get_many_user<'a, E>( user_id: UserId, exec: E, ) -> Result, sqlx::Error> - where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - let notification_ids = sqlx::query!( + use futures::stream::TryStreamExt; + + sqlx::query!( " - SELECT id - FROM notifications - WHERE user_id = $1 + SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, + STRING_AGG(DISTINCT na.id || ' |||| ' || na.title || ' |||| ' || na.action_route || ' |||| ' || na.action_route_method, ' ~~~~ ') actions + FROM notifications n + LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id + WHERE n.user_id = $1 + GROUP BY n.id, n.user_id; ", user_id as UserId ) - .fetch_all(exec) - .await? - .into_iter() - .map(|x| NotificationId(x.id)) - .collect(); + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|row| { + let id = NotificationId(row.id); + let mut actions: Vec = Vec::new(); - Self::get_many(notification_ids, exec).await + row.actions.unwrap_or_default().split(" ~~~~ ").for_each(|x| { + let action: Vec<&str> = x.split(" |||| ").collect(); + + if action.len() >= 3 { + actions.push(NotificationAction { + id: NotificationActionId(action[0].parse().unwrap_or(0)), + notification_id: id, + title: action[1].to_string(), + action_route_method: action[3].to_string(), + action_route: action[2].to_string(), + }); + } + }); + + Notification { + id, + user_id: UserId(row.user_id), + notification_type: row.notification_type, + title: row.title, + text: row.text, + link: row.link, + read: row.read, + created: row.created, + actions, + } + })) + }) + .try_collect::>() + .await } pub async fn remove( diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index becc0404b..4df126cce 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -1,4 +1,5 @@ use super::ids::*; +use crate::database::models::convert_postgres_date; use time::OffsetDateTime; #[derive(Clone, Debug)] @@ -719,11 +720,7 @@ impl Project { } else { Some(strings[4].to_string()) }, - created: OffsetDateTime::parse( - strings[2], - time::Format::Rfc3339, - ) - .unwrap_or_else(|_| OffsetDateTime::now_utc()), + created: convert_postgres_date(strings[2]), }) } else { None @@ -835,7 +832,7 @@ impl Project { featured: strings[1].parse().unwrap_or(false), title: if strings[3] == " " { None } else { Some(strings[3].to_string()) }, description: if strings[4] == " " { None } else { Some(strings[4].to_string()) }, - created: OffsetDateTime::parse(strings[2], time::Format::Rfc3339).unwrap_or_else(|_| OffsetDateTime::now_utc()) + created: convert_postgres_date(strings[2]) }) } else { None diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 3ddd20b9b..30e963bd4 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -1,5 +1,6 @@ use super::ids::*; use super::DatabaseError; +use crate::database::models::convert_postgres_date; use std::collections::HashMap; use time::OffsetDateTime; @@ -498,22 +499,20 @@ impl Version { let vec = sqlx::query!( " - SELECT version.id FROM ( - SELECT DISTINCT ON(v.id) v.id, v.date_published FROM versions v - INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id - INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[])) - INNER JOIN loaders_versions lv ON lv.version_id = v.id - INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[])) - WHERE v.mod_id = $1 - ) AS version - ORDER BY version.date_published ASC + SELECT DISTINCT ON(v.date_published, v.id) version_id, v.date_published FROM versions v + INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id + INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[])) + INNER JOIN loaders_versions lv ON lv.version_id = v.id + INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[])) + WHERE v.mod_id = $1 + ORDER BY v.date_published, v.id ASC ", project_id as ProjectId, &game_versions.unwrap_or_default(), &loaders.unwrap_or_default(), ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|v| VersionId(v.id))) }) + .try_filter_map(|e| async { Ok(e.right().map(|v| VersionId(v.version_id))) }) .try_collect::>() .await?; @@ -615,7 +614,7 @@ impl Version { SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number, v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads, v.version_type version_type, v.featured featured, - STRING_AGG(DISTINCT gv.version, ' ~~~~ ') game_versions, STRING_AGG(DISTINCT l.loader, ' ~~~~ ') loaders, + STRING_AGG(DISTINCT gv.version || ' |||| ' || gv.created, ' ~~~~ ') game_versions, STRING_AGG(DISTINCT l.loader, ' ~~~~ ') loaders, STRING_AGG(DISTINCT f.id || ' |||| ' || f.is_primary || ' |||| ' || f.size || ' |||| ' || f.url || ' |||| ' || f.filename, ' ~~~~ ') files, STRING_AGG(DISTINCT h.algorithm || ' |||| ' || encode(h.hash, 'escape') || ' |||| ' || h.file_id, ' ~~~~ ') hashes, STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ' |||| ' || COALESCE(d.mod_dependency_id, 0) || ' |||| ' || d.dependency_type || ' |||| ' || COALESCE(d.dependency_file_name, ' '), ' ~~~~ ') dependencies @@ -636,26 +635,6 @@ impl Version { .await?; if let Some(v) = result { - let hashes: Vec<(FileId, String, Vec)> = v - .hashes - .unwrap_or_default() - .split(" ~~~~ ") - .map(|f| { - let hash: Vec<&str> = f.split(" |||| ").collect(); - - if hash.len() >= 3 { - Some(( - FileId(hash[2].parse().unwrap_or(0)), - hash[0].to_string(), - hash[1].to_string().into_bytes(), - )) - } else { - None - } - }) - .flatten() - .collect(); - Ok(Some(QueryVersion { id: VersionId(v.id), project_id: ProjectId(v.mod_id), @@ -666,44 +645,87 @@ impl Version { changelog_url: v.changelog_url, date_published: v.date_published, downloads: v.downloads, - files: v - .files - .unwrap_or_default() - .split(" ~~~~ ") - .map(|f| { - let file: Vec<&str> = f.split(" |||| ").collect(); + files: { + let hashes: Vec<(FileId, String, Vec)> = v + .hashes + .unwrap_or_default() + .split(" ~~~~ ") + .map(|f| { + let hash: Vec<&str> = f.split(" |||| ").collect(); - if file.len() >= 5 { - let file_id = FileId(file[0].parse().unwrap_or(0)); - let mut file_hashes = HashMap::new(); - - for hash in &hashes { - if (hash.0).0 == file_id.0 { - file_hashes - .insert(hash.1.clone(), hash.2.clone()); - } + if hash.len() >= 3 { + Some(( + FileId(hash[2].parse().unwrap_or(0)), + hash[0].to_string(), + hash[1].to_string().into_bytes(), + )) + } else { + None } + }) + .flatten() + .collect(); - Some(QueryFile { - id: file_id, - url: file[3].to_string(), - filename: file[4].to_string(), - hashes: file_hashes, - primary: file[1].parse().unwrap_or(false), - size: file[2].parse().unwrap_or(0), - }) - } else { - None - } - }) - .flatten() - .collect(), - game_versions: v - .game_versions - .unwrap_or_default() - .split(" ~~~~ ") - .map(|x| x.to_string()) - .collect(), + v.files + .unwrap_or_default() + .split(" ~~~~ ") + .map(|f| { + let file: Vec<&str> = f.split(" |||| ").collect(); + + if file.len() >= 5 { + let file_id = + FileId(file[0].parse().unwrap_or(0)); + let mut file_hashes = HashMap::new(); + + for hash in &hashes { + if (hash.0).0 == file_id.0 { + file_hashes.insert( + hash.1.clone(), + hash.2.clone(), + ); + } + } + + Some(QueryFile { + id: file_id, + url: file[3].to_string(), + filename: file[4].to_string(), + hashes: file_hashes, + primary: file[1].parse().unwrap_or(false), + size: file[2].parse().unwrap_or(0), + }) + } else { + None + } + }) + .flatten() + .collect() + }, + game_versions: { + let game_versions = v.game_versions.unwrap_or_default(); + + let mut gv = game_versions + .split(" ~~~~ ") + .flat_map(|x| { + let version: Vec<&str> = + x.split(" |||| ").collect(); + + if version.len() >= 2 { + Some(( + version[0], + convert_postgres_date(version[1]) + .unix_timestamp(), + )) + } else { + None + } + }) + .collect::>(); + + gv.sort_by(|a, b| a.1.cmp(&b.1)); + + gv.into_iter().map(|x| x.0.to_string()).collect() + }, loaders: v .loaders .unwrap_or_default() @@ -770,7 +792,7 @@ impl Version { SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number, v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads, v.version_type version_type, v.featured featured, - STRING_AGG(DISTINCT gv.version, ' ~~~~ ') game_versions, STRING_AGG(DISTINCT l.loader, ' ~~~~ ') loaders, + STRING_AGG(DISTINCT gv.version || ' |||| ' || gv.created, ' ~~~~ ') game_versions, STRING_AGG(DISTINCT l.loader, ' ~~~~ ') loaders, STRING_AGG(DISTINCT f.id || ' |||| ' || f.is_primary || ' |||| ' || f.size || ' |||| ' || f.url || ' |||| ' || f.filename, ' ~~~~ ') files, STRING_AGG(DISTINCT h.algorithm || ' |||| ' || encode(h.hash, 'escape') || ' |||| ' || h.file_id, ' ~~~~ ') hashes, STRING_AGG(DISTINCT COALESCE(d.dependency_id, 0) || ' |||| ' || COALESCE(d.mod_dependency_id, 0) || ' |||| ' || d.dependency_type || ' |||| ' || COALESCE(d.dependency_file_name, ' '), ' ~~~~ ') dependencies @@ -790,21 +812,7 @@ impl Version { ) .fetch_many(exec) .try_filter_map(|e| async { - Ok(e.right().map(|v| { - let hashes: Vec<(FileId, String, Vec)> = v.hashes.unwrap_or_default().split(" ~~~~ ").map(|f| { - let hash: Vec<&str> = f.split(" |||| ").collect(); - - if hash.len() >= 3 { - Some(( - FileId(hash[2].parse().unwrap_or(0)), - hash[0].to_string(), - hash[1].to_string().into_bytes(), - )) - } else { - None - } - }).flatten().collect(); - + Ok(e.right().map(|v| QueryVersion { id: VersionId(v.id), project_id: ProjectId(v.mod_id), @@ -815,32 +823,71 @@ impl Version { changelog_url: v.changelog_url, date_published: v.date_published, downloads: v.downloads, - files: v.files.unwrap_or_default().split(" ~~~~ ").map(|f| { - let file: Vec<&str> = f.split(" |||| ").collect(); + files: { + let hashes: Vec<(FileId, String, Vec)> = v.hashes.unwrap_or_default().split(" ~~~~ ").map(|f| { + let hash: Vec<&str> = f.split(" |||| ").collect(); - if file.len() >= 5 { - let file_id = FileId(file[0].parse().unwrap_or(0)); - let mut file_hashes = HashMap::new(); - - for hash in &hashes { - if (hash.0).0 == file_id.0 { - file_hashes.insert(hash.1.clone(), hash.2.clone()); - } + if hash.len() >= 3 { + Some(( + FileId(hash[2].parse().unwrap_or(0)), + hash[0].to_string(), + hash[1].to_string().into_bytes(), + )) + } else { + None } + }).flatten().collect(); - Some(QueryFile { - id: file_id, - url: file[3].to_string(), - filename: file[4].to_string(), - hashes: file_hashes, - primary: file[1].parse().unwrap_or(false), - size: file[2].parse().unwrap_or(0), + v.files.unwrap_or_default().split(" ~~~~ ").map(|f| { + let file: Vec<&str> = f.split(" |||| ").collect(); + + if file.len() >= 5 { + let file_id = FileId(file[0].parse().unwrap_or(0)); + let mut file_hashes = HashMap::new(); + + for hash in &hashes { + if (hash.0).0 == file_id.0 { + file_hashes.insert(hash.1.clone(), hash.2.clone()); + } + } + + Some(QueryFile { + id: file_id, + url: file[3].to_string(), + filename: file[4].to_string(), + hashes: file_hashes, + primary: file[1].parse().unwrap_or(false), + size: file[2].parse().unwrap_or(0), + }) + } else { + None + } + }).flatten().collect() + }, + game_versions: { + let game_versions = v + .game_versions + .unwrap_or_default(); + + let mut gv = game_versions + .split(" ~~~~ ") + .flat_map(|x| { + let version: Vec<&str> = x.split(" |||| ").collect(); + + if version.len() >= 2 { + Some((version[0], convert_postgres_date(version[1]).unix_timestamp())) + } else { + None + } }) - } else { - None - } - }).flatten().collect(), - game_versions: v.game_versions.unwrap_or_default().split(" ~~~~ ").map(|x| x.to_string()).collect(), + .collect::>(); + + gv.sort_by(|a, b| a.1.cmp(&b.1)); + + gv.into_iter() + .map(|x| x.0.to_string()) + .collect() + }, loaders: v.loaders.unwrap_or_default().split(" ~~~~ ").map(|x| x.to_string()).collect(), featured: v.featured, dependencies: v.dependencies @@ -878,7 +925,7 @@ impl Version { }).flatten().collect(), version_type: v.version_type } - })) + )) }) .try_collect::>() .await diff --git a/src/main.rs b/src/main.rs index b206b3ae3..d666c684f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -253,9 +253,7 @@ async fn main() -> std::io::Result<()> { }) .with_interval(std::time::Duration::from_secs(60)) .with_max_requests(300) - .with_ignore_key( - dotenv::var("RATE_LIMIT_IGNORE_KEY").ok(), - ), + .with_ignore_key(dotenv::var("RATE_LIMIT_IGNORE_KEY").ok()), ) .app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(file_host.clone())) @@ -296,6 +294,11 @@ fn check_env_vars() -> bool { failed |= true; } + if parse_strings_from_var("ALLOWED_CALLBACK_URLS").is_none() { + warn!("Variable `ALLOWED_CALLBACK_URLS` missing in dotenv or not a json array of strings"); + failed |= true; + } + failed |= check_var::("SITE_URL"); failed |= check_var::("CDN_URL"); failed |= check_var::("LABRINTH_ADMIN_KEY"); diff --git a/src/ratelimit/middleware.rs b/src/ratelimit/middleware.rs index 1080f67f8..e4bf8d6e7 100644 --- a/src/ratelimit/middleware.rs +++ b/src/ratelimit/middleware.rs @@ -51,7 +51,7 @@ where max_requests: 0, store, identifier: Rc::new(Box::new(identifier)), - ignore_key: None + ignore_key: None, } } diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 8cea1f4a5..97a95565d 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -1,3 +1,16 @@ + /*! +This auth module is primarily for use within the main website. Applications interacting with the +authenticated API (a very small portion - notifications, private projects, editing/creating projects +and versions) should either retrieve the Modrinth GitHub token through the site, or create a personal +app token for use with Modrinth. + +JUst as a summary: Don't implement this flow in your application! Instead, use a personal access token +or create your own GitHub OAuth2 application. + +This system will be revisited and allow easier interaction with the authenticated API once we roll +out our own authentication system. +*/ + use crate::database::models::{generate_state_id, User}; use crate::models::error::ApiError; use crate::models::ids::base62_impl::{parse_base62, to_base62}; @@ -11,6 +24,7 @@ use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; use thiserror::Error; use time::OffsetDateTime; +use crate::parse_strings_from_var; pub fn config(cfg: &mut ServiceConfig) { cfg.service(scope("auth").service(auth_callback).service(init)); @@ -34,6 +48,8 @@ pub enum AuthorizationError { Authentication(#[from] crate::util::auth::AuthenticationError), #[error("Error while decoding Base62")] Decoding(#[from] DecodingError), + #[error("Invalid callback URL specified")] + Url, } impl actix_web::ResponseError for AuthorizationError { fn status_code(&self) -> StatusCode { @@ -50,6 +66,7 @@ impl actix_web::ResponseError for AuthorizationError { AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED, AuthorizationError::Decoding(..) => StatusCode::BAD_REQUEST, AuthorizationError::Authentication(..) => StatusCode::UNAUTHORIZED, + AuthorizationError::Url => StatusCode::BAD_REQUEST, } } @@ -65,7 +82,8 @@ impl actix_web::ResponseError for AuthorizationError { AuthorizationError::Decoding(..) => "decoding_error", AuthorizationError::Authentication(..) => { "authentication_error" - } + }, + AuthorizationError::Url => "url_error", }, description: &self.to_string(), }) @@ -96,6 +114,16 @@ pub async fn init( Query(info): Query, client: Data, ) -> Result { + let url = url::Url::parse(&info.url).map_err(|_| AuthorizationError::Url)?; + + let allowed_callback_urls = parse_strings_from_var("ALLOWED_CALLBACK_URLS") + .unwrap_or_default(); + + let domain = url.domain().ok_or(AuthorizationError::Url)?; + if !allowed_callback_urls.iter().any(|x| domain.ends_with(x)) { + return Err(AuthorizationError::Url); + } + let mut transaction = client.begin().await?; let state = generate_state_id(&mut transaction).await?; @@ -136,7 +164,7 @@ pub async fn auth_callback( let result_option = sqlx::query!( " - SELECT url,expires FROM states + SELECT url, expires FROM states WHERE id = $1 ", state_id as i64 @@ -145,13 +173,11 @@ pub async fn auth_callback( .await?; if let Some(result) = result_option { - // let now = OffsetDateTime::now_utc(); - // TODO: redo this condition later.. - // let duration = now - result.expires; - // - // if duration.whole_seconds() < 0 { - // return Err(AuthorizationError::InvalidCredentials); - // } + let duration = result.expires - OffsetDateTime::now_utc(); + + if duration.whole_seconds() < 0 { + return Err(AuthorizationError::InvalidCredentials); + } sqlx::query!( " diff --git a/src/routes/teams.rs b/src/routes/teams.rs index e98f05db6..44a001a56 100644 --- a/src/routes/teams.rs +++ b/src/routes/teams.rs @@ -385,18 +385,29 @@ pub async fn transfer_ownership( let id = info.into_inner().0; let current_user = get_user_from_headers(req.headers(), &**pool).await?; - let member = TeamMember::get_from_user_id( - id.into(), - current_user.id.into(), - &**pool, - ) - .await? - .ok_or_else(|| { - ApiError::CustomAuthentication( - "You don't have permission to edit members of this team" - .to_string(), + + if !current_user.role.is_mod() { + let member = TeamMember::get_from_user_id( + id.into(), + current_user.id.into(), + &**pool, ) - })?; + .await? + .ok_or_else(|| { + ApiError::CustomAuthentication( + "You don't have permission to edit members of this team" + .to_string(), + ) + })?; + + if member.role != crate::models::teams::OWNER_ROLE { + return Err(ApiError::CustomAuthentication( + "You don't have permission to edit the ownership of this team" + .to_string(), + )); + } + } + let new_member = TeamMember::get_from_user_id( id.into(), new_owner.user_id.into(), @@ -409,13 +420,6 @@ pub async fn transfer_ownership( ) })?; - if member.role != crate::models::teams::OWNER_ROLE { - return Err(ApiError::CustomAuthentication( - "You don't have permission to edit the ownership of this team" - .to_string(), - )); - } - if !new_member.accepted { return Err(ApiError::InvalidInput( "You can only transfer ownership to members who are currently in your team".to_string(), diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index d1ea00157..9f1a59f22 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -35,7 +35,7 @@ pub struct InitialVersionData { regex = "crate::util::validate::RE_URL_SAFE" )] pub version_number: String, - #[validate(length(min = 3, max = 256))] + #[validate(length(min = 1, max = 256))] #[serde(alias = "name")] pub version_title: String, #[validate(length(max = 65536))] @@ -639,11 +639,11 @@ pub async fn upload_file( field: &mut Field, file_host: &dyn FileHost, uploaded_files: &mut Vec, - version_files: &mut Vec, - dependencies: &mut Vec, + version_files: &mut Vec, + dependencies: &mut Vec, cdn_url: &str, content_disposition: &actix_web::http::header::ContentDisposition, - project_id: crate::models::ids::ProjectId, + project_id: ProjectId, version_number: &str, project_type: &str, loaders: Vec, diff --git a/src/routes/version_file.rs b/src/routes/version_file.rs index 3eddab6a2..7a695b657 100644 --- a/src/routes/version_file.rs +++ b/src/routes/version_file.rs @@ -303,7 +303,7 @@ pub async fn get_versions_from_hashes( let hashes_parsed: Vec> = file_data .hashes .iter() - .map(|x| x.as_bytes().to_vec()) + .map(|x| x.to_lowercase().as_bytes().to_vec()) .collect(); let result = sqlx::query!( @@ -360,7 +360,7 @@ pub async fn download_files( let hashes_parsed: Vec> = file_data .hashes .iter() - .map(|x| x.as_bytes().to_vec()) + .map(|x| x.to_lowercase().as_bytes().to_vec()) .collect(); let mut transaction = pool.begin().await?; @@ -411,7 +411,7 @@ pub async fn update_files( let hashes_parsed: Vec> = update_data .hashes .iter() - .map(|x| x.as_bytes().to_vec()) + .map(|x| x.to_lowercase().as_bytes().to_vec()) .collect(); let mut transaction = pool.begin().await?; diff --git a/src/validate/pack.rs b/src/validate/pack.rs index ff79daff3..bec42a5d4 100644 --- a/src/validate/pack.rs +++ b/src/validate/pack.rs @@ -20,7 +20,7 @@ impl super::Validator for PackValidator { } fn get_supported_loaders(&self) -> &[&str] { - &["forge", "fabric"] + &["forge", "fabric", "quilt"] } fn get_supported_game_versions(&self) -> SupportedGameVersions {