diff --git a/.env b/.env index 381839e55..65ae7a675 100644 --- a/.env +++ b/.env @@ -22,7 +22,6 @@ S3_SECRET=none S3_URL=none S3_REGION=none S3_BUCKET_NAME=none -S3_PROVIDER=none # 1 hour LOCAL_INDEX_INTERVAL=3600 diff --git a/Cargo.lock b/Cargo.lock index 7d604855f..7cfa56b9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1835,6 +1835,7 @@ dependencies = [ "serde_json", "serde_with", "sha1", + "sha2", "sqlx", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index c0cc2cf53..8532813a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ chrono = { version = "0.4", features = ["serde"] } rand = "0.7.3" base64 = "0.13.0" sha1 = { version = "0.6.0", features = ["std"] } +sha2 = "0.9.2" bitflags = "1.2.1" gumdrop = "0.8.0" diff --git a/migrations/20210113202021_add-descriptions.sql b/migrations/20210113202021_add-descriptions.sql new file mode 100644 index 000000000..5046c9906 --- /dev/null +++ b/migrations/20210113202021_add-descriptions.sql @@ -0,0 +1,15 @@ +ALTER TABLE mods + ADD COLUMN body varchar(65536) NOT NULL DEFAULT ''; +ALTER TABLE mods + ALTER COLUMN body_url DROP NOT NULL; +ALTER TABLE versions + ADD COLUMN changelog varchar(65536) NOT NULL DEFAULT ''; + +INSERT INTO users ( + id, github_id, username, name, email, + avatar_url, bio, created +) +VALUES ( + 127155982985829, 10137, 'Ghost', NULL, NULL, + 'https://avatars2.githubusercontent.com/u/10137', 'A deleted user', NOW() + ); \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index ac3cbb576..500cc20c1 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -262,6 +262,19 @@ ] } }, + "16049957962ded08751d5a4ddce2ffac17ecd486f61210c51a952508425d83e6": { + "query": "\n UPDATE versions\n SET changelog = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "16871e66d8762452be3ca0c80f4733f2db49980205fbf7cb6f9829cdd99cdb65": { "query": "\n INSERT INTO dependencies (dependent_id, dependency_id)\n VALUES ($1, $2)\n ", "describe": { @@ -441,35 +454,6 @@ ] } }, - "1fff206f9fce8353668875267a5ac31a3610f0d6a2131c2230e3c98c6e6b963a": { - "query": "\n INSERT INTO mods (\n id, team_id, title, description, body_url,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, discord_url,\n client_side, server_side, license_url, license,\n slug\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13,\n $14, $15, $16, $17,\n $18\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar", - "Varchar", - "Varchar", - "Timestamptz", - "Int4", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Int4", - "Varchar", - "Int4", - "Int4", - "Varchar", - "Int4", - "Varchar" - ] - }, - "nullable": [] - } - }, "2439ae8db5ae81aad03351e9a2e19259ef033110ed1c2d71bc0a643104ca32d0": { "query": "\n UPDATE versions\n SET downloads = downloads + 1\n WHERE id = $1\n ", "describe": { @@ -583,86 +567,6 @@ "nullable": [] } }, - "2f5ce51392035ccfd846b16824ad6ee607de87bb55e7152a51f21035f83021c7": { - "query": "\n SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n v.release_channel, v.accepted, v.featured\n FROM versions v\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\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": "name", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "changelog_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 8, - "name": "release_channel", - "type_info": "Int4" - }, - { - "ordinal": 9, - "name": "accepted", - "type_info": "Bool" - }, - { - "ordinal": 10, - "name": "featured", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false - ] - } - }, "2fa070eef3fe8f708a1495104f78eda2bfa0fe19ada2bf66ac35fb2468631774": { "query": "\n SELECT category FROM categories\n ", "describe": { @@ -747,6 +651,92 @@ "nullable": [] } }, + "35a05ad89abf739b345f9544b57fe439cd364a9969bb02953e5d9faec697ae97": { + "query": "\n SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog, v.changelog_url, v.date_published, v.downloads,\n v.release_channel, v.accepted, v.featured\n FROM versions v\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\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": "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": "release_channel", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "accepted", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "featured", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + } + }, "3831c1b321e47690f1f54597506a0d43362eda9540c56acb19c06532bba50b01": { "query": "\n SELECT id, user_id, role, permissions, accepted\n FROM team_members\n WHERE team_id = $1\n ", "describe": { @@ -888,26 +878,6 @@ "nullable": [] } }, - "42e072309779598d0c213280dd8052d1b4889cb24ef5204ca13b74f693b94328": { - "query": "\n SELECT user_id FROM team_members tm\n INNER JOIN mods ON mods.team_id = tm.team_id\n WHERE mods.id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "user_id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "43b793e2df30a6ace9e037e38bb4ea456656cfbe276c151e3a9e0a408d2c249f": { "query": "\n UPDATE versions\n SET release_channel = $1\n WHERE (id = $2)\n ", "describe": { @@ -969,80 +939,6 @@ "nullable": [] } }, - "4805a7c9f1dbc9b16cff88da43ba506dd5cfa2b96adca596929579ef336fba6d": { - "query": "\n SELECT v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n release_channels.channel, v.accepted, v.featured\n FROM versions v\n INNER JOIN release_channels ON v.release_channel = release_channels.id\n WHERE v.id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "author_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "changelog_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 7, - "name": "channel", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "accepted", - "type_info": "Bool" - }, - { - "ordinal": 9, - "name": "featured", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - false, - false, - false - ] - } - }, "486c13abb9648b16a4c354f25754408d649b13980fe7486cdcfdcacfa2725c2b": { "query": "\n SELECT * FROM versions\n WHERE accepted = FALSE\n ORDER BY date_published ASC\n LIMIT $1;\n ", "describe": { @@ -1101,6 +997,11 @@ "ordinal": 10, "name": "featured", "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "changelog", + "type_info": "Varchar" } ], "parameters": { @@ -1119,6 +1020,7 @@ false, false, false, + false, false ] } @@ -1143,6 +1045,27 @@ ] } }, + "4a4b4166248877eefcd63603945fdcd392f76812bdec7c70f8ffeb06ee7e737f": { + "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id\n WHERE tm.user_id = $1 AND tm.role = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "4a54d350b4695c32a802675506e85b0506fc62a63ca0ee5f38890824301d6515": { "query": "\n UPDATE mods\n SET server_side = $1\n WHERE (id = $2)\n ", "describe": { @@ -1313,6 +1236,57 @@ "nullable": [] } }, + "5564434408e4b88ff1bdd14e0d32a35136e5ee0c837655fbde7d3ca9182dc25b": { + "query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE\n WHERE m.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "role", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "permissions", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accepted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + } + }, "560c3ba57c965c3ebdbe393b062da8a30a8a7116a9bace2aa7de2e8431fe0bc7": { "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id)\n VALUES ($1, $2)\n ", "describe": { @@ -1545,6 +1519,11 @@ "ordinal": 18, "name": "slug", "type_info": "Varchar" + }, + { + "ordinal": 19, + "name": "body", + "type_info": "Varchar" } ], "parameters": { @@ -1558,7 +1537,7 @@ false, false, false, - false, + true, false, false, true, @@ -1572,7 +1551,8 @@ false, false, true, - true + true, + false ] } }, @@ -1670,128 +1650,6 @@ "nullable": [] } }, - "71b0c906abfb16d399e23d1b6afbecb5e55c6e9b65cdc8de7d4fbf9ece682d03": { - "query": "\n SELECT title, description, downloads,\n icon_url, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 8, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 14, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 15, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 16, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 17, - "name": "slug", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - true, - false, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true - ] - } - }, "71db1bc306ff6da3a92544e1585aa11c5627b50d95b15e794b2fa5dc838ea1a3": { "query": "\n SELECT mod_id, version_number, author_id\n FROM versions\n WHERE id = $1\n ", "describe": { @@ -2048,6 +1906,57 @@ ] } }, + "82cc64ff6fc37cd52a6dee033d1d571a3e570abe0aa10aea9860cdb8d1ea8cdc": { + "query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted FROM versions v\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE\n WHERE v.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "role", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "permissions", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "accepted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + } + }, "89310b2bc5f020744a9a42dae6f15dfebc1544cdd754939f0d09714353f2aa7c": { "query": "\n SELECT id, team_id, role, permissions, accepted\n FROM team_members\n WHERE user_id = $1\n ", "describe": { @@ -2131,6 +2040,140 @@ "nullable": [] } }, + "904b88f0b2ae62a31c79e11c63e1ca42fd6c61c330de33b0f62f6d675c3e3df0": { + "query": "\n SELECT id, title, description, downloads,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 16, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 18, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "slug", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true + ] + } + }, "97143e41c18d191d09d244113b7b6cdf5bd6ab89c62ac46d0980d700ab288f48": { "query": "\n SELECT name FROM side_types\n WHERE id = $1\n ", "describe": { @@ -2191,6 +2234,214 @@ ] } }, + "a08d75ec11e3cec9826d6968c0e953fe5d6627660384d066e4628dd9c79ceeeb": { + "query": "\n SELECT v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog, v.changelog_url, v.date_published, v.downloads,\n v.release_channel, v.accepted, v.featured\n FROM versions v\n WHERE v.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "changelog", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "release_channel", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "accepted", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "featured", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + } + }, + "a2369858ff21b3fa491685168624e9eb22aca34ef0fefc774675b38066c8beba": { + "query": "\n SELECT title, description, downloads,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 15, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 16, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 17, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 18, + "name": "slug", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true + ] + } + }, "a28188c4840d0f3449379b3bba6b3c4af9483e01f50fd56785317398a59881ca": { "query": "\n SELECT files.id, files.url, files.filename, files.is_primary FROM files\n WHERE files.version_id = $1\n ", "describe": { @@ -2414,134 +2665,6 @@ ] } }, - "aac56ba5384e3b2353c1adb359856fe149963488f751f1f7eb056455874e994c": { - "query": "\n SELECT id, title, description, downloads,\n icon_url, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 9, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 15, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 16, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 17, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 18, - "name": "slug", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true - ] - } - }, "acbafe265c4b7a1c95b0494a0a03c8bd2cd778ae561ef5a662fa931ca26cf603": { "query": "\n DELETE FROM mods_donations\n WHERE joining_mod_id = $1\n ", "describe": { @@ -2661,7 +2784,7 @@ false, false, true, - false, + true, false, false, false, @@ -2690,6 +2813,31 @@ ] } }, + "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169": { + "query": "\n DELETE FROM users\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "b7b2b5b99340c7601de53cc33dc56af054b50b2fe4d1d212901c958115a42baa": { + "query": "\n UPDATE versions\n SET author_id = $1\n WHERE (author_id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [] + } + }, "b903ac4e686ef85ba28d698c668da07860e7f276b261d8f2cebb74e73b094970": { "query": "\n DELETE FROM hashes\n WHERE EXISTS(\n SELECT 1 FROM files WHERE\n (files.version_id = $1) AND\n (hashes.file_id = files.id)\n )\n ", "describe": { @@ -3130,6 +3278,20 @@ ] } }, + "ccdbb0f1206dfe7963777f0f8edd40b57b870460fed97bf547eb6bc20a1359e6": { + "query": "\n UPDATE team_members\n SET user_id = $1\n WHERE (user_id = $2 AND role = $3)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text" + ] + }, + "nullable": [] + } + }, "cdf37a51c68eaa7401da19c5b1e34097592027ceb777bfa535116e3a52b28958": { "query": "\n SELECT joining_platform_id, url FROM mods_donations\n WHERE joining_mod_id = $1\n ", "describe": { @@ -3314,6 +3476,19 @@ ] } }, + "dc6aa2e7bfd5d5004620ddd4cd6a47ecc56159e1489054e0652d56df802fb5e5": { + "query": "\n UPDATE mods\n SET body = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, "deb81673526789bca38d39e64303f61d2a63febfdfb68136e58517af9f7792bc": { "query": "\n SELECT category FROM mods_categories\n INNER JOIN categories ON joining_category_id = id\n WHERE joining_mod_id = $1\n ", "describe": { @@ -3405,8 +3580,8 @@ "nullable": [] } }, - "e6a5d7cde6fa3103752065e2c5febee39afe548ab5ec8860fcc7e6ff1ad3f42f": { - "query": "\n SELECT v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n v.release_channel, v.accepted, v.featured\n FROM versions v\n WHERE v.id = $1\n ", + "e7558867b28d7760ca10063374dbfdb9c5b625a4ba039cab38582a6ab6767a24": { + "query": "\n SELECT v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog, v.changelog_url, v.date_published, v.downloads,\n release_channels.channel, v.accepted, v.featured\n FROM versions v\n INNER JOIN release_channels ON v.release_channel = release_channels.id\n WHERE v.id = $1\n ", "describe": { "columns": [ { @@ -3431,31 +3606,36 @@ }, { "ordinal": 4, - "name": "changelog_url", + "name": "changelog", "type_info": "Varchar" }, { "ordinal": 5, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 6, "name": "date_published", "type_info": "Timestamptz" }, { - "ordinal": 6, + "ordinal": 7, "name": "downloads", "type_info": "Int4" }, { - "ordinal": 7, - "name": "release_channel", - "type_info": "Int4" + "ordinal": 8, + "name": "channel", + "type_info": "Varchar" }, { - "ordinal": 8, + "ordinal": 9, "name": "accepted", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "featured", "type_info": "Bool" } @@ -3470,6 +3650,7 @@ false, false, false, + false, true, false, false, @@ -3692,6 +3873,18 @@ ] } }, + "f22e9aee090f9952cf795a3540c03b0a5036dab0b740847d05e03d4565756283": { + "query": "\n DELETE FROM team_members\n WHERE user_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "f453b43772c4d2d9d09dc389eb95482cc75e7f0eaf9dc7ff48cf40f22f1497cc": { "query": "\n UPDATE users\n SET bio = $1\n WHERE (id = $2)\n ", "describe": { @@ -3705,6 +3898,35 @@ "nullable": [] } }, + "f481818cf577979ca751681155dada6ffee92ac6179e39c38f9c0442a33eac2e": { + "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, discord_url,\n client_side, server_side, license_url, license,\n slug\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13,\n $14, $15, $16, $17,\n $18\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Int4", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Int4", + "Varchar", + "Int4", + "Int4", + "Varchar", + "Int4", + "Varchar" + ] + }, + "nullable": [] + } + }, "f741273df7b987cbe2be4db3c3a11cf18027dfedb61dff41b9226aaa793b90cb": { "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug\n FROM mods m\n WHERE id = $1\n ", "describe": { @@ -3771,7 +3993,7 @@ false, false, true, - false, + true, false, false, false, diff --git a/src/database/models/mod_item.rs b/src/database/models/mod_item.rs index 7f7f2b687..03928ad13 100644 --- a/src/database/models/mod_item.rs +++ b/src/database/models/mod_item.rs @@ -36,7 +36,7 @@ pub struct ModBuilder { pub team_id: TeamId, pub title: String, pub description: String, - pub body_url: String, + pub body: String, pub icon_url: Option, pub issues_url: Option, pub source_url: Option, @@ -63,7 +63,8 @@ impl ModBuilder { team_id: self.team_id, title: self.title, description: self.description, - body_url: self.body_url, + body: self.body, + body_url: None, published: chrono::Utc::now(), updated: chrono::Utc::now(), status: self.status, @@ -113,7 +114,8 @@ pub struct Mod { pub team_id: TeamId, pub title: String, pub description: String, - pub body_url: String, + pub body: String, + pub body_url: Option, pub published: chrono::DateTime, pub updated: chrono::DateTime, pub status: StatusId, @@ -138,7 +140,7 @@ impl Mod { sqlx::query!( " INSERT INTO mods ( - id, team_id, title, description, body_url, + id, team_id, title, description, body, published, downloads, icon_url, issues_url, source_url, wiki_url, status, discord_url, client_side, server_side, license_url, license, @@ -156,7 +158,7 @@ impl Mod { self.team_id as TeamId, &self.title, &self.description, - &self.body_url, + &self.body, self.published, self.downloads, self.icon_url.as_ref(), @@ -184,7 +186,7 @@ impl Mod { let result = sqlx::query!( " SELECT title, description, downloads, - icon_url, body_url, published, + icon_url, body, body_url, published, updated, status, issues_url, source_url, wiki_url, discord_url, license_url, team_id, client_side, server_side, license, slug @@ -217,6 +219,7 @@ impl Mod { server_side: SideTypeId(row.server_side), license: LicenseId(row.license), slug: row.slug, + body: row.body, })) } else { Ok(None) @@ -233,7 +236,7 @@ impl Mod { let mods = sqlx::query!( " SELECT id, title, description, downloads, - icon_url, body_url, published, + icon_url, body, body_url, published, updated, status, issues_url, source_url, wiki_url, discord_url, license_url, team_id, client_side, server_side, license, slug @@ -264,6 +267,7 @@ impl Mod { server_side: SideTypeId(m.server_side), license: LicenseId(m.license), slug: m.slug, + body: m.body, })) }) .try_collect::>() diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index a5d297438..267b338d0 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -411,4 +411,75 @@ impl TeamMember { Ok(()) } + + pub async fn get_from_user_id_mod<'a, 'b, E>( + id: ModId, + user_id: UserId, + executor: E, + ) -> Result, super::DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted FROM mods m + INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE + WHERE m.id = $1 + ", + id as ModId, + user_id as UserId + ) + .fetch_optional(executor) + .await?; + + if let Some(m) = result { + Ok(Some(TeamMember { + id: TeamMemberId(m.id), + team_id: TeamId(m.team_id), + user_id, + role: m.role, + permissions: Permissions::from_bits(m.permissions as u64) + .ok_or(super::DatabaseError::BitflagError)?, + accepted: m.accepted, + })) + } else { + Ok(None) + } + } + + pub async fn get_from_user_id_version<'a, 'b, E>( + id: VersionId, + user_id: UserId, + executor: E, + ) -> Result, super::DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.accepted FROM versions v + INNER JOIN mods m ON m.id = v.mod_id + INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE + WHERE v.id = $1 + ", + id as VersionId, + user_id as UserId + ) + .fetch_optional(executor) + .await?; + + if let Some(m) = result { + Ok(Some(TeamMember { + id: TeamMemberId(m.id), + team_id: TeamId(m.team_id), + user_id, + role: m.role, + permissions: Permissions::from_bits(m.permissions as u64) + .ok_or(super::DatabaseError::BitflagError)?, + accepted: m.accepted, + })) + } else { + Ok(None) + } + } } diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 6f8caff1e..cb96a9f3a 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -207,4 +207,121 @@ impl User { Ok(mods) } + + pub async fn remove<'a, 'b, E>(id: UserId, exec: E) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + let deleted_user: UserId = crate::models::users::DELETED_USER.into(); + + sqlx::query!( + " + UPDATE team_members + SET user_id = $1 + WHERE (user_id = $2 AND role = $3) + ", + deleted_user as UserId, + id as UserId, + crate::models::teams::OWNER_ROLE + ) + .execute(exec) + .await?; + + sqlx::query!( + " + UPDATE versions + SET author_id = $1 + WHERE (author_id = $2) + ", + deleted_user as UserId, + id as UserId, + ) + .execute(exec) + .await?; + + sqlx::query!( + " + DELETE FROM team_members + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(exec) + .await?; + + sqlx::query!( + " + DELETE FROM users + WHERE id = $1 + ", + id as UserId, + ) + .execute(exec) + .await?; + + Ok(Some(())) + } + + pub async fn remove_full<'a, 'b, E>( + id: UserId, + exec: E, + ) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + use futures::TryStreamExt; + let mods: Vec = sqlx::query!( + " + SELECT m.id FROM mods m + INNER JOIN team_members tm ON tm.team_id = m.team_id + WHERE tm.user_id = $1 AND tm.role = $2 + ", + id as UserId, + crate::models::teams::OWNER_ROLE + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) }) + .try_collect::>() + .await?; + + for mod_id in mods { + let _result = super::mod_item::Mod::remove_full(mod_id, exec).await?; + } + + let deleted_user: UserId = crate::models::users::DELETED_USER.into(); + + sqlx::query!( + " + UPDATE versions + SET author_id = $1 + WHERE (author_id = $2) + ", + deleted_user as UserId, + id as UserId, + ) + .execute(exec) + .await?; + + sqlx::query!( + " + DELETE FROM team_members + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(exec) + .await?; + + sqlx::query!( + " + DELETE FROM users + WHERE id = $1 + ", + id as UserId, + ) + .execute(exec) + .await?; + + Ok(Some(())) + } } diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index b3723ffdd..46cfc7850 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -7,7 +7,7 @@ pub struct VersionBuilder { pub author_id: UserId, pub name: String, pub version_number: String, - pub changelog_url: Option, + pub changelog: String, pub files: Vec, pub dependencies: Vec, pub game_versions: Vec, @@ -78,7 +78,8 @@ impl VersionBuilder { author_id: self.author_id, name: self.name, version_number: self.version_number, - changelog_url: self.changelog_url, + changelog: self.changelog, + changelog_url: None, date_published: chrono::Utc::now(), downloads: 0, release_channel: self.release_channel, @@ -152,6 +153,7 @@ pub struct Version { pub author_id: UserId, pub name: String, pub version_number: String, + pub changelog: String, pub changelog_url: Option, pub date_published: chrono::DateTime, pub downloads: i32, @@ -382,7 +384,7 @@ impl Version { let result = sqlx::query!( " SELECT v.mod_id, v.author_id, v.name, v.version_number, - v.changelog_url, v.date_published, v.downloads, + v.changelog, v.changelog_url, v.date_published, v.downloads, v.release_channel, v.accepted, v.featured FROM versions v WHERE v.id = $1 @@ -399,6 +401,7 @@ impl Version { author_id: UserId(row.author_id), name: row.name, version_number: row.version_number, + changelog: row.changelog, changelog_url: row.changelog_url, date_published: row.date_published, downloads: row.downloads, @@ -424,7 +427,7 @@ impl Version { let versions = sqlx::query!( " SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number, - v.changelog_url, v.date_published, v.downloads, + v.changelog, v.changelog_url, v.date_published, v.downloads, v.release_channel, v.accepted, v.featured FROM versions v WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[])) @@ -439,6 +442,7 @@ impl Version { author_id: UserId(v.author_id), name: v.name, version_number: v.version_number, + changelog: v.changelog, changelog_url: v.changelog_url, date_published: v.date_published, downloads: v.downloads, @@ -463,7 +467,7 @@ impl Version { let result = sqlx::query!( " SELECT v.mod_id, v.author_id, v.name, v.version_number, - v.changelog_url, v.date_published, v.downloads, + v.changelog, v.changelog_url, v.date_published, v.downloads, release_channels.channel, v.accepted, v.featured FROM versions v INNER JOIN release_channels ON v.release_channel = release_channels.id @@ -545,6 +549,7 @@ impl Version { author_id: UserId(row.author_id), name: row.name, version_number: row.version_number, + changelog: row.changelog, changelog_url: row.changelog_url, date_published: row.date_published, downloads: row.downloads, @@ -599,6 +604,7 @@ pub struct QueryVersion { pub author_id: UserId, pub name: String, pub version_number: String, + pub changelog: String, pub changelog_url: Option, pub date_published: chrono::DateTime, pub downloads: i32, diff --git a/src/file_hosting/backblaze.rs b/src/file_hosting/backblaze.rs index 32899a750..06065db08 100644 --- a/src/file_hosting/backblaze.rs +++ b/src/file_hosting/backblaze.rs @@ -1,5 +1,6 @@ use super::{DeleteFileData, FileHost, FileHostingError, UploadFileData}; use async_trait::async_trait; +use sha2::Digest; mod authorization; mod delete; @@ -32,12 +33,15 @@ impl FileHost for BackblazeHost { file_name: &str, file_bytes: Vec, ) -> Result { + let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); + let upload_data = upload::upload_file(&self.upload_url_data, content_type, file_name, file_bytes).await?; Ok(UploadFileData { file_id: upload_data.file_id, file_name: upload_data.file_name, content_length: upload_data.content_length, + content_sha512, content_sha1: upload_data.content_sha1, content_md5: upload_data.content_md5, content_type: upload_data.content_type, diff --git a/src/file_hosting/mock.rs b/src/file_hosting/mock.rs index da14255ad..20bf2689b 100644 --- a/src/file_hosting/mock.rs +++ b/src/file_hosting/mock.rs @@ -1,5 +1,6 @@ use super::{DeleteFileData, FileHost, FileHostingError, UploadFileData}; use async_trait::async_trait; +use sha2::Digest; pub struct MockHost(()); @@ -21,12 +22,14 @@ impl FileHost for MockHost { .join(file_name.replace("../", "")); std::fs::create_dir_all(path.parent().ok_or(FileHostingError::InvalidFilename)?)?; let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest(); + let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); std::fs::write(path, &file_bytes)?; Ok(UploadFileData { file_id: String::from("MOCK_FILE_ID"), file_name: file_name.to_string(), content_length: file_bytes.len() as u32, + content_sha512, content_sha1, content_md5: None, content_type: content_type.to_string(), diff --git a/src/file_hosting/mod.rs b/src/file_hosting/mod.rs index f40436ead..336cda91e 100644 --- a/src/file_hosting/mod.rs +++ b/src/file_hosting/mod.rs @@ -32,6 +32,7 @@ pub struct UploadFileData { pub file_id: String, pub file_name: String, pub content_length: u32, + pub content_sha512: String, pub content_sha1: String, pub content_md5: Option, pub content_type: String, diff --git a/src/file_hosting/s3_host.rs b/src/file_hosting/s3_host.rs index 1596c8975..d8a2b3960 100644 --- a/src/file_hosting/s3_host.rs +++ b/src/file_hosting/s3_host.rs @@ -3,6 +3,7 @@ use async_trait::async_trait; use s3::bucket::Bucket; use s3::creds::Credentials; use s3::region::Region; +use sha2::Digest; pub struct S3Host { bucket: Bucket, @@ -40,6 +41,7 @@ impl FileHost for S3Host { file_bytes: Vec, ) -> Result { let content_sha1 = sha1::Sha1::from(&file_bytes).hexdigest(); + let content_sha512 = format!("{:x}", sha2::Sha512::digest(&file_bytes)); self.bucket .put_object_with_content_type( @@ -49,37 +51,11 @@ impl FileHost for S3Host { ) .await?; - let provider = &*dotenv::var("S3_PROVIDER").unwrap(); - - if provider == "do" { - reqwest::Client::new() - .delete(&*format!( - "https://api.digitalocean.com/v2/cdn/endpoints/{}/cache", - self.bucket.name - )) - .header(reqwest::header::CONTENT_TYPE, "application/json") - .header( - reqwest::header::AUTHORIZATION, - self.bucket - .credentials - .secret_key - .clone() - .unwrap_or_else(|| "".to_string()), - ) - .body( - serde_json::json!({ - "files": vec![file_name], - }) - .to_string(), - ) - .send() - .await?; - } - Ok(UploadFileData { file_id: file_name.to_string(), file_name: file_name.to_string(), content_length: file_bytes.len() as u32, + content_sha512, content_sha1, content_md5: None, content_type: content_type.to_string(), diff --git a/src/main.rs b/src/main.rs index 2437b554b..4c6beebc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -356,7 +356,6 @@ fn check_env_vars() -> bool { failed |= check_var::("S3_URL"); failed |= check_var::("S3_REGION"); failed |= check_var::("S3_BUCKET_NAME"); - failed |= check_var::("S3_PROVIDER"); } else if storage_backend.as_deref() == Some("local") { failed |= check_var::("MOCK_FILE_PATH"); } else if let Some(backend) = storage_backend { diff --git a/src/models/mods.rs b/src/models/mods.rs index d53f89b4c..a15633061 100644 --- a/src/models/mods.rs +++ b/src/models/mods.rs @@ -29,8 +29,10 @@ pub struct Mod { pub title: String, /// A short description of the mod. pub description: String, - /// The link to the long description of the mod. - pub body_url: String, + /// A long form description of the mod. + pub body: String, + /// The link to the long description of the mod. (Deprecated), being replaced by `body` + pub body_url: Option, /// The date at which the mod was first published. pub published: DateTime, /// The date at which the mod was first published. @@ -192,7 +194,9 @@ pub struct Version { pub name: String, /// The version number. Ideally will follow semantic versioning pub version_number: String, - /// A link to the changelog for this version of the mod. + /// The changelog for this version of the mod. + pub changelog: String, + /// A link to the changelog for this version of the mod. (Deprecated), being replaced by `changelog` pub changelog_url: Option, /// The date that this version was published. pub date_published: DateTime, diff --git a/src/models/users.rs b/src/models/users.rs index 67f5850f3..c94d8e796 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize}; #[serde(into = "Base62Id")] pub struct UserId(pub u64); +pub const DELETED_USER: UserId = UserId(127155982985829); + #[derive(Serialize, Deserialize)] pub struct User { pub id: UserId, diff --git a/src/routes/mod_creation.rs b/src/routes/mod_creation.rs index ee59e5f80..6f1e21797 100644 --- a/src/routes/mod_creation.rs +++ b/src/routes/mod_creation.rs @@ -48,6 +48,8 @@ pub enum CreateError { InvalidFileType(String), #[error("Authentication Error: {0}")] Unauthorized(#[from] AuthenticationError), + #[error("Authentication Error: {0}")] + CustomAuthenticationError(String), } impl actix_web::ResponseError for CreateError { @@ -68,6 +70,7 @@ impl actix_web::ResponseError for CreateError { CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST, CreateError::InvalidFileType(..) => StatusCode::BAD_REQUEST, CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED, + CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED, } } @@ -89,6 +92,7 @@ impl actix_web::ResponseError for CreateError { CreateError::InvalidCategory(..) => "invalid_input", CreateError::InvalidFileType(..) => "invalid_input", CreateError::Unauthorized(..) => "unauthorized", + CreateError::CustomAuthenticationError(..) => "unauthorized", }, description: &self.to_string(), }) @@ -334,18 +338,8 @@ async fn mod_create_inner( ))); } } - versions.push( - create_initial_version( - data, - mod_id, - current_user.id, - &cdn_url, - transaction, - file_host, - uploaded_files, - ) - .await?, - ); + versions + .push(create_initial_version(data, mod_id, current_user.id, transaction).await?); } mod_create_data = create_data; @@ -436,24 +430,6 @@ async fn mod_create_inner( categories.push(id); } - // Upload the mod desciption markdown to the CDN - // TODO: Should we also process and upload an html version here for SSR? - let body_path = format!("data/{}/description.md", mod_id); - { - let upload_data = file_host - .upload_file( - "text/plain", - &body_path, - mod_create_data.mod_body.into_bytes(), - ) - .await?; - - uploaded_files.push(UploadedFile { - file_id: upload_data.file_id, - file_name: upload_data.file_name, - }); - } - let team = models::team_item::TeamBuilder { members: vec![models::team_item::TeamMemberBuilder { user_id: current_user.id.into(), @@ -527,7 +503,7 @@ async fn mod_create_inner( team_id, title: mod_create_data.mod_name, description: mod_create_data.mod_description, - body_url: format!("{}/{}", cdn_url, body_path), + body: mod_create_data.mod_body, icon_url, issues_url: mod_create_data.issues_url, source_url: mod_create_data.source_url, @@ -553,7 +529,8 @@ async fn mod_create_inner( team: team_id.into(), title: mod_builder.title.clone(), description: mod_builder.description.clone(), - body_url: mod_builder.body_url.clone(), + body: mod_builder.body.clone(), + body_url: None, published: now, updated: now, status: status.clone(), @@ -596,10 +573,7 @@ async fn create_initial_version( version_data: &InitialVersionData, mod_id: ModId, author: UserId, - cdn_url: &str, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - file_host: &dyn FileHost, - uploaded_files: &mut Vec, ) -> Result { if version_data.mod_id.is_some() { return Err(CreateError::InvalidInput(String::from( @@ -613,30 +587,6 @@ async fn create_initial_version( // Randomly generate a new id to be used for the version let version_id: VersionId = models::generate_version_id(transaction).await?.into(); - // Upload the version's changelog to the CDN - let changelog_path = if let Some(changelog) = &version_data.version_body { - let changelog_path = format!( - "data/{}/versions/{}/changelog.md", - mod_id, version_data.version_number - ); - - let uploaded_text = file_host - .upload_file( - "text/plain", - &changelog_path, - changelog.clone().into_bytes(), - ) - .await?; - - uploaded_files.push(UploadedFile { - file_id: uploaded_text.file_id, - file_name: uploaded_text.file_name, - }); - Some(changelog_path) - } else { - None - }; - let release_channel = models::ChannelId::get_id(version_data.release_channel.as_str(), &mut *transaction) .await? @@ -670,7 +620,10 @@ async fn create_initial_version( author_id: author.into(), name: version_data.version_title.clone(), version_number: version_data.version_number.clone(), - changelog_url: changelog_path.map(|path| format!("{}/{}", cdn_url, path)), + changelog: version_data + .version_body + .clone() + .unwrap_or_else(|| "".to_string()), files: Vec::new(), dependencies, game_versions, diff --git a/src/routes/moderation.rs b/src/routes/moderation.rs index 4a36eebba..9c256176d 100644 --- a/src/routes/moderation.rs +++ b/src/routes/moderation.rs @@ -32,8 +32,8 @@ pub struct ModerationMod { pub title: String, /// A short description of the mod. pub description: String, - /// The link to the long description of the mod. - pub body_url: String, + /// The long description of the mod. + pub body: String, /// The date at which the mod was first published. pub published: DateTime, /// The date at which the mod was first published. @@ -85,7 +85,7 @@ pub async fn mods( team: database::models::ids::TeamId(m.team_id).into(), title: m.title, description: m.description, - body_url: m.body_url, + body: m.body, published: m.published, icon_url: m.icon_url, issues_url: m.issues_url, @@ -133,6 +133,7 @@ pub async fn versions( featured: m.featured, name: m.name, version_number: m.version_number, + changelog: m.changelog, changelog_url: m.changelog_url, date_published: m.date_published, downloads: m.downloads as u32, diff --git a/src/routes/mods.rs b/src/routes/mods.rs index 7c9f7a23c..3f95c93c5 100644 --- a/src/routes/mods.rs +++ b/src/routes/mods.rs @@ -203,6 +203,7 @@ fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod team: m.team_id.into(), title: m.title, description: m.description, + body: m.body, body_url: m.body_url, published: m.published, updated: m.updated, @@ -282,7 +283,6 @@ pub async fn mod_edit( info: web::Path<(models::ids::ModId,)>, pool: web::Data, config: web::Data, - file_host: web::Data>, new_mod: web::Json, indexing_queue: Data>, ) -> Result { @@ -729,13 +729,18 @@ pub async fn mod_edit( )); } - let body_path = format!("data/{}/description.md", mod_id); - - file_host.delete_file_version("", &*body_path).await?; - - file_host - .upload_file("text/plain", &body_path, body.clone().into_bytes()) - .await?; + sqlx::query!( + " + UPDATE mods + SET body = $1 + WHERE (id = $2) + ", + body, + id as database::models::ids::ModId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; } transaction @@ -857,18 +862,13 @@ pub async fn mod_delete( let id = info.into_inner().0; if !user.role.is_mod() { - let mod_item = database::models::Mod::get(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?; - let team_member = database::models::TeamMember::get_from_user_id( - mod_item.team_id, - user.id.into(), - &**pool, - ) - .await - .map_err(ApiError::DatabaseError)? - .ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?; + let team_member = + database::models::TeamMember::get_from_user_id_mod(id.into(), user.id.into(), &**pool) + .await + .map_err(ApiError::DatabaseError)? + .ok_or_else(|| { + ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()) + })?; if !team_member.permissions.contains(Permissions::DELETE_MOD) { return Err(ApiError::CustomAuthenticationError( diff --git a/src/routes/teams.rs b/src/routes/teams.rs index 075854819..d3aee4a9c 100644 --- a/src/routes/teams.rs +++ b/src/routes/teams.rs @@ -287,7 +287,8 @@ pub async fn remove_team_member( let user_id = ids.1.into(); let current_user = get_user_from_headers(req.headers(), &**pool).await?; - let team_member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?; + let team_member = + TeamMember::get_from_user_id_pending(id, current_user.id.into(), &**pool).await?; let member = match team_member { Some(m) => m, @@ -312,7 +313,7 @@ pub async fn remove_team_member( // Members other than the owner can either leave the team, or be // removed by a member with the REMOVE_MEMBER permission. if delete_member.user_id == member.user_id - || member.permissions.contains(Permissions::REMOVE_MEMBER) + || (member.permissions.contains(Permissions::REMOVE_MEMBER) && member.accepted) { TeamMember::delete(id, user_id, &**pool).await?; } else { @@ -321,7 +322,7 @@ pub async fn remove_team_member( )); } } else if delete_member.user_id == member.user_id - || member.permissions.contains(Permissions::MANAGE_INVITES) + || (member.permissions.contains(Permissions::MANAGE_INVITES) && member.accepted) { // This is a pending invite rather than a member, so the // user being invited or team members with the MANAGE_INVITES diff --git a/src/routes/users.rs b/src/routes/users.rs index f699f6091..e477859c6 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,4 +1,4 @@ -use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; +use crate::auth::get_user_from_headers; use crate::database::models::{TeamMember, User}; use crate::file_hosting::FileHost; use crate::models::users::{Role, UserId}; @@ -420,24 +420,42 @@ pub async fn user_icon_edit( } } -// TODO: Make this actually do stuff +#[derive(Deserialize)] +pub struct RemovalType { + #[serde(default = "default_removal")] + removal_type: String, +} + +fn default_removal() -> String { + "partial".into() +} + #[delete("{id}")] pub async fn user_delete( req: HttpRequest, info: web::Path<(UserId,)>, pool: web::Data, + removal_type: web::Query, ) -> Result { - check_is_moderator_from_headers( - req.headers(), - &mut *pool - .acquire() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?, - ) - .await?; + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id = info.into_inner().0; - let _id = info.0; - let result = Some(()); + if !user.role.is_mod() && user.id == user.id { + return Err(ApiError::CustomAuthenticationError( + "You do not have permission to delete this user!".to_string(), + )); + } + + let result; + if removal_type.removal_type == "full".to_string() { + result = crate::database::models::User::remove_full(id.into(), &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } else { + result = crate::database::models::User::remove(id.into(), &**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + }; if result.is_some() { Ok(HttpResponse::Ok().body("")) diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index 0de69eec1..3f83bf126 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -5,6 +5,7 @@ use crate::file_hosting::FileHost; use crate::models::mods::{ GameVersion, ModId, ModLoader, Version, VersionFile, VersionId, VersionType, }; +use crate::models::teams::Permissions; use crate::routes::mod_creation::{CreateError, UploadedFile}; use actix_multipart::{Field, Multipart}; use actix_web::web::Data; @@ -180,53 +181,29 @@ async fn version_create_inner( // Check that the user creating this version is a team member // of the mod the version is being added to. - let member_ids = sqlx::query!( - " - SELECT user_id FROM team_members tm - INNER JOIN mods ON mods.team_id = tm.team_id - WHERE mods.id = $1 - ", - mod_id as models::ModId, + let team_member = models::TeamMember::get_from_user_id_mod( + mod_id.into(), + user.id.into(), + &mut *transaction, ) - .fetch_all(&mut *transaction) - .await?; + .await? + .ok_or_else(|| { + CreateError::CustomAuthenticationError( + "You don't have permission to upload this version!".to_string(), + ) + })?; - let member_ids: Vec = member_ids - .iter() - .map(|m| models::UserId(m.user_id)) - .collect(); - - if !member_ids.contains(&user.id.into()) { - // TODO: Some team members may not have the permissions - // to upload mods; We need a more in depth permissions - // system. - return Err(CreateError::InvalidInput("Unauthorized".to_string())); + if !team_member + .permissions + .contains(Permissions::UPLOAD_VERSION) + { + return Err(CreateError::CustomAuthenticationError( + "You don't have permission to upload this version!".to_string(), + )); } let version_id: VersionId = models::generate_version_id(transaction).await?.into(); - let body_path; - - if let Some(body) = &version_create_data.version_body { - let path = format!( - "data/{}/versions/{}/changelog.md", - version_create_data.mod_id.unwrap(), - version_create_data.version_number - ); - - let uploaded_text = file_host - .upload_file("text/plain", &path, body.clone().into_bytes()) - .await?; - - uploaded_files.push(UploadedFile { - file_id: uploaded_text.file_id, - file_name: uploaded_text.file_name.clone(), - }); - body_path = Some(path); - } else { - body_path = None; - } - let release_channel = models::ChannelId::get_id( version_create_data.release_channel.as_str(), &mut *transaction, @@ -256,7 +233,10 @@ async fn version_create_inner( author_id: user.id.into(), name: version_create_data.version_title.clone(), version_number: version_create_data.version_number.clone(), - changelog_url: body_path.map(|path| format!("{}/{}", cdn_url, path)), + changelog: version_create_data + .version_body + .clone() + .unwrap_or_else(|| "".to_string()), files: Vec::new(), dependencies: version_create_data .dependencies @@ -303,7 +283,8 @@ async fn version_create_inner( featured: builder.featured, name: builder.name.clone(), version_number: builder.version_number.clone(), - changelog_url: builder.changelog_url.clone(), + changelog: builder.changelog.clone(), + changelog_url: None, date_published: chrono::Utc::now(), downloads: 0, version_type: version_data.release_channel, @@ -418,8 +399,25 @@ async fn upload_file_to_version_inner( } }; - if version.author_id as u64 != user.id.0 { - return Err(CreateError::InvalidInput("Unauthorized".to_string())); + let team_member = models::TeamMember::get_from_user_id_version( + version_id.into(), + user.id.into(), + &mut *transaction, + ) + .await? + .ok_or_else(|| { + CreateError::CustomAuthenticationError( + "You don't have permission to upload files to this version!".to_string(), + ) + })?; + + if team_member + .permissions + .contains(Permissions::UPLOAD_VERSION) + { + return Err(CreateError::CustomAuthenticationError( + "You don't have permission to upload files to this version!".to_string(), + )); } let mod_id = ModId(version.mod_id as u64); @@ -526,12 +524,20 @@ pub async fn upload_file( Ok(models::version_item::VersionFileBuilder { filename: file_name.to_string(), url: format!("{}/{}", cdn_url, upload_data.file_name), - hashes: vec![models::version_item::HashBuilder { - algorithm: "sha1".to_string(), - // This is an invalid cast - the database expects the hash's - // bytes, but this is the string version. - hash: upload_data.content_sha1.into_bytes(), - }], + hashes: vec![ + models::version_item::HashBuilder { + algorithm: "sha1".to_string(), + // This is an invalid cast - the database expects the hash's + // bytes, but this is the string version. + hash: upload_data.content_sha1.into_bytes(), + }, + models::version_item::HashBuilder { + algorithm: "sha512".to_string(), + // This is an invalid cast - the database expects the hash's + // bytes, but this is the string version. + hash: upload_data.content_sha512.into_bytes(), + }, + ], primary: uploaded_files.len() == 1, }) } diff --git a/src/routes/versions.rs b/src/routes/versions.rs index 1c2461c1b..d300af239 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -1,5 +1,5 @@ use super::ApiError; -use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; +use crate::auth::get_user_from_headers; use crate::file_hosting::FileHost; use crate::models; use crate::models::teams::Permissions; @@ -159,6 +159,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models featured: data.featured, name: data.name, version_number: data.version_number, + changelog: data.changelog, changelog_url: data.changelog_url, date_published: data.date_published, downloads: data.downloads as u32, @@ -220,7 +221,6 @@ pub async fn version_edit( req: HttpRequest, info: web::Path<(models::ids::VersionId,)>, pool: web::Data, - file_host: web::Data>, new_version: web::Json, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; @@ -233,18 +233,8 @@ pub async fn version_edit( .map_err(|e| ApiError::DatabaseError(e.into()))?; if let Some(version_item) = result { - let mod_item = database::models::Mod::get(version_item.mod_id, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .ok_or_else(|| { - ApiError::InvalidInputError( - "Attempted to edit version not attached to mod. How did this happen?" - .to_string(), - ) - })?; - - let team_member = database::models::TeamMember::get_from_user_id( - mod_item.team_id, + let team_member = database::models::TeamMember::get_from_user_id_version( + version_item.id, user.id.into(), &**pool, ) @@ -494,17 +484,18 @@ pub async fn version_edit( } if let Some(body) = &new_version.changelog { - let mod_id: models::mods::ModId = version_item.mod_id.into(); - let body_path = format!( - "data/{}/versions/{}/changelog.md", - mod_id, version_item.version_number - ); - - file_host.delete_file_version("", &*body_path).await?; - - file_host - .upload_file("text/plain", &body_path, body.clone().into_bytes()) - .await?; + sqlx::query!( + " + UPDATE versions + SET changelog = $1 + WHERE (id = $2) + ", + body, + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; } transaction @@ -532,20 +523,8 @@ pub async fn version_delete( let id = info.into_inner().0; if !user.role.is_mod() { - let version = database::models::Version::get(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .ok_or_else(|| { - ApiError::InvalidInputError("An invalid version ID was specified".to_string()) - })?; - let mod_item = database::models::Mod::get(version.mod_id, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .ok_or_else(|| { - ApiError::InvalidInputError("The version is not attached to a mod".to_string()) - })?; - let team_member = database::models::TeamMember::get_from_user_id( - mod_item.team_id, + let team_member = database::models::TeamMember::get_from_user_id_version( + id.into(), user.id.into(), &**pool, ) @@ -735,7 +714,7 @@ pub async fn delete_file( file_host: web::Data>, algorithm: web::Query, ) -> Result { - check_is_moderator_from_headers(req.headers(), &**pool).await?; + let user = get_user_from_headers(req.headers(), &**pool).await?; let hash = info.into_inner().0; @@ -754,6 +733,30 @@ pub async fn delete_file( .map_err(|e| ApiError::DatabaseError(e.into()))?; if let Some(row) = result { + if !user.role.is_mod() { + let team_member = database::models::TeamMember::get_from_user_id_version( + database::models::ids::VersionId(row.version_id), + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::DatabaseError)? + .ok_or_else(|| { + ApiError::CustomAuthenticationError( + "You don't have permission to delete this file!".to_string(), + ) + })?; + + if !team_member + .permissions + .contains(Permissions::DELETE_VERSION) + { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to delete this file!".to_string(), + )); + } + } + let mut transaction = pool .begin() .await