From 7fbb8838e7a2fb55a5eda52f049e2751b1809bc7 Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:44:40 -0700 Subject: [PATCH] Scoped PATs (#651) * Scoped PATs * fix threads issues * fix migration --- Cargo.lock | 60 +- Cargo.toml | 4 +- migrations/20230710034250_flows.sql | 44 + sqlx-data.json | 1285 ++++++++++++++------------- src/auth/mod.rs | 2 +- src/auth/pat.rs | 115 --- src/auth/pats.rs | 269 ++++++ src/auth/session.rs | 33 +- src/auth/validate.rs | 66 +- src/database/models/ids.rs | 21 +- src/database/models/mod.rs | 1 + src/database/models/pat_item.rs | 289 ++++++ src/database/models/project_item.rs | 27 +- src/database/models/report_item.rs | 29 +- src/database/models/session_item.rs | 4 +- src/database/models/thread_item.rs | 18 +- src/database/models/version_item.rs | 6 +- src/main.rs | 4 +- src/models/ids.rs | 2 + src/models/mod.rs | 1 + src/models/pats.rs | 137 +++ src/models/projects.rs | 4 +- src/models/reports.rs | 4 +- src/models/teams.rs | 1 - src/models/threads.rs | 5 + src/queue/session.rs | 61 +- src/routes/maven.rs | 63 +- src/routes/updates.rs | 18 +- src/routes/v2/mod.rs | 2 +- src/routes/v2/moderation.rs | 4 +- src/routes/v2/notifications.rs | 75 +- src/routes/v2/pats.rs | 249 ------ src/routes/v2/project_creation.rs | 45 +- src/routes/v2/projects.rs | 198 ++++- src/routes/v2/reports.rs | 104 ++- src/routes/v2/tags.rs | 2 - src/routes/v2/teams.rs | 108 ++- src/routes/v2/threads.rs | 184 ++-- src/routes/v2/users.rs | 183 ++-- src/routes/v2/version_creation.rs | 31 +- src/routes/v2/version_file.rs | 105 ++- src/routes/v2/versions.rs | 99 ++- 42 files changed, 2560 insertions(+), 1402 deletions(-) create mode 100644 migrations/20230710034250_flows.sql delete mode 100644 src/auth/pat.rs create mode 100644 src/auth/pats.rs create mode 100644 src/database/models/pat_item.rs create mode 100644 src/models/pats.rs delete mode 100644 src/routes/v2/pats.rs diff --git a/Cargo.lock b/Cargo.lock index c8fcf8e4f..d0430a620 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1280,6 +1280,22 @@ dependencies = [ "serde", ] +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64 0.21.2", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -1874,6 +1890,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.4.0" @@ -2093,6 +2119,7 @@ dependencies = [ "image", "itertools 0.11.0", "lazy_static", + "lettre", "log", "meilisearch-sdk", "rand", @@ -2144,6 +2171,29 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +[[package]] +name = "lettre" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d" +dependencies = [ + "base64 0.21.2", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna 0.3.0", + "mime", + "native-tls", + "nom 7.1.3", + "once_cell", + "quoted_printable", + "socket2", + "tokio", +] + [[package]] name = "lexical-core" version = "0.7.6" @@ -2978,6 +3028,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49" + [[package]] name = "r2d2" version = "0.8.10" @@ -4408,7 +4464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", - "idna", + "idna 0.4.0", "percent-encoding", "serde", ] @@ -4441,7 +4497,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" dependencies = [ - "idna", + "idna 0.4.0", "lazy_static", "phonenumber", "regex", diff --git a/Cargo.toml b/Cargo.toml index 0a34c14a9..c22947347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,4 +79,6 @@ sentry-actix = "0.31.5" image = "0.24.6" color-thief = "0.2.2" -woothee = "0.13.0" \ No newline at end of file +woothee = "0.13.0" + +lettre = "0.10.4" \ No newline at end of file diff --git a/migrations/20230710034250_flows.sql b/migrations/20230710034250_flows.sql new file mode 100644 index 000000000..6fb0f1067 --- /dev/null +++ b/migrations/20230710034250_flows.sql @@ -0,0 +1,44 @@ +CREATE INDEX sessions_session + ON sessions (session); + +CREATE TABLE flows ( + id bigint NOT NULL PRIMARY KEY, + flow varchar(64) NOT NULL UNIQUE, + user_id BIGINT NOT NULL REFERENCES users(id), + expires timestamptz NOT NULL, + flow_type varchar(64) NOT NULL +); + +CREATE INDEX flows_flow + ON flows (flow); + +DROP TABLE pats; + +CREATE TABLE pats ( + id BIGINT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + user_id BIGINT NOT NULL REFERENCES users(id), + access_token VARCHAR(64) NOT NULL UNIQUE, + scopes BIGINT NOT NULL, + created timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires timestamptz NOT NULL, + last_used timestamptz NULL +); + +CREATE INDEX pats_user_id + ON pats (user_id); + +CREATE INDEX pats_access_token + ON pats (access_token); + +ALTER TABLE mods DROP COLUMN thread_id; +ALTER TABLE reports DROP COLUMN thread_id; + +DELETE FROM threads_members; +DELETE FROM threads_messages; +DELETE FROM threads; + +ALTER TABLE threads + ADD COLUMN report_id bigint references reports ON UPDATE CASCADE NULL; +ALTER TABLE threads + ADD COLUMN mod_id bigint references mods ON UPDATE CASCADE NULL; \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index 798c3fa51..d35a4122f 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -87,49 +87,18 @@ }, "query": "\n UPDATE mods\n SET source_url = $1\n WHERE (id = $2)\n " }, - "04a6310fcc8183e6795a63a5017d607174a9a94587ce774e124613b184dfff43": { + "0472045549758d8eef84592908c438d6222a26926f4b06865b84979fc92564ba": { "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "thread_type", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "show_in_mod_inbox", - "ordinal": 2, - "type_info": "Bool" - }, - { - "name": "members", - "ordinal": 3, - "type_info": "Int8Array" - }, - { - "name": "messages", - "ordinal": 4, - "type_info": "Jsonb" - } - ], - "nullable": [ - false, - false, - false, - null, - null - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Int8Array" + "Int8", + "Timestamptz" ] } }, - "query": "\n SELECT t.id, t.thread_type, t.show_in_mod_inbox,\n ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members,\n JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created)) filter (where tmsg.id is not null) messages\n FROM threads t\n LEFT OUTER JOIN threads_messages tmsg ON tmsg.thread_id = t.id\n LEFT OUTER JOIN threads_members tm ON tm.thread_id = t.id\n WHERE t.id = ANY($1)\n GROUP BY t.id\n " + "query": "\n UPDATE pats\n SET last_used = $2\n WHERE (id = $1)\n " }, "04e5ecb14c526000e9098efb65861f6125e6fcc88f39d6ad811ac8504d229de1": { "describe": { @@ -557,57 +526,6 @@ }, "query": "\n UPDATE notifications\n SET read = TRUE\n WHERE id = ANY($1)\n " }, - "13e9d01d815b415eb8505e2362319cbdb7881f100f80671289f47886f3ed084e": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "status", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "team_id", - "ordinal": 2, - "type_info": "Int8" - } - ], - "nullable": [ - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "SELECT m.id, m.status, m.team_id FROM mods m WHERE thread_id = $1" - }, - "1411c9ae3af067679aa21d7f45937cd94d457e4eb17a108566776a9bd1ee77e2": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int4", - "Int8", - "Int8", - "Int8", - "Varchar", - "Int8", - "Int8" - ] - } - }, - "query": "\n INSERT INTO reports (\n id, report_type_id, mod_id, version_id, user_id,\n body, reporter, thread_id\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8\n )\n " - }, "1510e820cdc31b62222d2be6a838e69876fe3c3c742dea188ca1a6b7f894d610": { "describe": { "columns": [ @@ -813,6 +731,27 @@ }, "query": "\n SELECT mod_id FROM versions WHERE id = $1\n " }, + "165a4e679a0063dbf20832f298b4af3bb350f2e7128b0a91d6c1b8a25e56b0f6": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM reports WHERE id = $1 AND reporter = $2)" + }, "166d93a7d4ac629444eadcd51d793490220bbf1e503bf85ec97b37500c8f74aa": { "describe": { "columns": [], @@ -1000,6 +939,26 @@ }, "query": "\n UPDATE threads_messages\n SET body = $2\n WHERE id = $1\n " }, + "21d20e5f09cb0729dc16c8609c35cec5a913f3172b53b8ae05da0096a33b4b64": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT id\n FROM pats\n WHERE user_id = $1\n ORDER BY created DESC\n " + }, "21ef50f46b7b3e62b91e7d067c1cb33806e14c33bb76d63c2711f822c44261f6": { "describe": { "columns": [ @@ -1106,47 +1065,6 @@ }, "query": "\n SELECT m.id id, tm.user_id user_id, tm.payouts_split payouts_split\n FROM mods m\n INNER JOIN team_members tm on m.team_id = tm.team_id AND tm.accepted = TRUE\n WHERE m.id = ANY($1)\n " }, - "281e3faffa65b51fadc93108ccc93d3d19934c8f26efb568f4794e4c6f16cefe": { - "describe": { - "columns": [ - { - "name": "thread_id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8Array", - "Int8" - ] - } - }, - "query": "\n SELECT m.thread_id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2\n WHERE m.thread_id = ANY($1)\n " - }, - "28b9d32b6d200f34e86f890ce477be0b8717f7ad92dc9cffa56eda4b12ee0df2": { - "describe": { - "columns": [ - { - "name": "thread_id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT thread_id FROM reports\n WHERE id = $1\n " - }, "294f264382ad55475b51776cd5d306c4867e8e6966ab79921bba69dc023f8337": { "describe": { "columns": [], @@ -1484,93 +1402,6 @@ }, "query": "\n UPDATE mods\n SET follows = follows - 1\n WHERE id = $1\n " }, - "39b32e9fad8113bd3ddb97fa555524a61e0d8d8d50d2b6cf1f829ebac290be56": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "mod_id", - "ordinal": 2, - "type_info": "Int8" - }, - { - "name": "version_id", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "user_id", - "ordinal": 4, - "type_info": "Int8" - }, - { - "name": "body", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "reporter", - "ordinal": 6, - "type_info": "Int8" - }, - { - "name": "created", - "ordinal": 7, - "type_info": "Timestamptz" - }, - { - "name": "thread_id", - "ordinal": 8, - "type_info": "Int8" - }, - { - "name": "closed", - "ordinal": 9, - "type_info": "Bool" - } - ], - "nullable": [ - false, - false, - true, - true, - true, - false, - false, - false, - true, - false - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, r.thread_id, r.closed\n FROM reports r\n INNER JOIN report_types rt ON rt.id = r.report_type_id\n WHERE r.id = ANY($1)\n ORDER BY r.created DESC\n " - }, - "3ae7c4a29dab8bce0e84a9c47a4a4f50a3be4bcb86e5b13d7dd60975d62e9ea3": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Varchar" - ] - } - }, - "query": "\n INSERT INTO threads (\n id, thread_type\n )\n VALUES (\n $1, $2\n )\n " - }, "3af747b5543a5a9b10dcce0a1eb9c2a1926dd5a507fe0d8b7f52d8ccc7fcd0af": { "describe": { "columns": [], @@ -1958,6 +1789,21 @@ }, "query": "\n DELETE FROM game_versions_versions WHERE joining_version_id = $1\n " }, + "50e65ff5df36ec59c5cf4470db908d7b04cf1ffb1640398ac518510178fd9a34": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Int8", + "Int8" + ] + } + }, + "query": "\n INSERT INTO threads (\n id, thread_type, mod_id, report_id\n )\n VALUES (\n $1, $2, $3, $4\n )\n " + }, "5295fba2053675c8414c0b37a59943535b9a438a642ea1c68045e987f05ade13": { "describe": { "columns": [ @@ -2047,6 +1893,26 @@ }, "query": "\n SELECT mod_id FROM versions WHERE id = $1\n " }, + "5944eb30a2bc0381c4d15eb1cf6ccf6e146a54381f2da8ab224960430e951976": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT id FROM threads\n WHERE report_id = $1\n " + }, "599a7966e054d7892c6c48c6f303872bb51f2b5eb387a3967bf8aebb5d33f627": { "describe": { "columns": [ @@ -2504,26 +2370,79 @@ }, "query": "\n SELECT f.id id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n WHERE h.algorithm = $2 AND h.hash = $1\n " }, - "6c4a42c263ae2787744aa6903e3cd85e90beaa5bea7ba78b45dbf55ce007753d": { + "6b7958eac5f273af8f37c0c888594e106fe323cbb3b0c32868b02f869d30f33f": { "describe": { "columns": [ { - "name": "exists", + "name": "id", "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "mod_id", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "version_id", + "ordinal": 3, + "type_info": "Int8" + }, + { + "name": "user_id", + "ordinal": 4, + "type_info": "Int8" + }, + { + "name": "body", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "reporter", + "ordinal": 6, + "type_info": "Int8" + }, + { + "name": "created", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "thread_id", + "ordinal": 8, + "type_info": "Int8" + }, + { + "name": "closed", + "ordinal": 9, "type_info": "Bool" } ], "nullable": [ - null + false, + false, + true, + true, + true, + false, + false, + false, + false, + false ], "parameters": { "Left": [ - "Int8", - "Int8" + "Int8Array" ] } }, - "query": "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 WHERE thread_id = $1)" + "query": "\n SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, t.id thread_id, r.closed\n FROM reports r\n INNER JOIN report_types rt ON rt.id = r.report_type_id\n INNER JOIN threads t ON t.report_id = r.id\n WHERE r.id = ANY($1)\n ORDER BY r.created DESC\n " }, "6c7aeb0db4a4fb3387c37b8d7aca6fdafaa637fd883a44416b56270aeebb7a01": { "describe": { @@ -2678,26 +2597,6 @@ }, "query": "\n INSERT INTO threads_members (\n thread_id, user_id\n )\n VALUES (\n $1, $2\n )\n " }, - "71766ebbecc05c27ae368992f6ec1043661deb57ba62b7f2328016df9c055cba": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "SELECT r.id FROM reports r WHERE thread_id = $1" - }, "71abd207410d123f9a50345ddcddee335fea0d0cc6f28762713ee01a36aee8a0": { "describe": { "columns": [ @@ -2953,6 +2852,27 @@ }, "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)" }, + "797cddf8f779025726a4a42c42985b8bc4c14094b76d9cd66dca20a7da3dec2a": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 WHERE m.id = $1)" + }, "79b896b1a8ddab285294638302976b75d0d915f36036383cc21bd2fc48d4502c": { "describe": { "columns": [], @@ -3064,262 +2984,6 @@ }, "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)\n VALUES ($1, $2, FALSE)\n " }, - "7d74e978a3d1bd16c4c3cad4bbb34e64191bae273591019497c8e151a837a5b0": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "project_type", - "ordinal": 1, - "type_info": "Int4" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "description", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "downloads", - "ordinal": 4, - "type_info": "Int4" - }, - { - "name": "follows", - "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "icon_url", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "body", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "published", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "updated", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "approved", - "ordinal": 10, - "type_info": "Timestamptz" - }, - { - "name": "queued", - "ordinal": 11, - "type_info": "Timestamptz" - }, - { - "name": "status", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "requested_status", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "issues_url", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "source_url", - "ordinal": 15, - "type_info": "Varchar" - }, - { - "name": "wiki_url", - "ordinal": 16, - "type_info": "Varchar" - }, - { - "name": "discord_url", - "ordinal": 17, - "type_info": "Varchar" - }, - { - "name": "license_url", - "ordinal": 18, - "type_info": "Varchar" - }, - { - "name": "team_id", - "ordinal": 19, - "type_info": "Int8" - }, - { - "name": "client_side", - "ordinal": 20, - "type_info": "Int4" - }, - { - "name": "server_side", - "ordinal": 21, - "type_info": "Int4" - }, - { - "name": "license", - "ordinal": 22, - "type_info": "Varchar" - }, - { - "name": "slug", - "ordinal": 23, - "type_info": "Varchar" - }, - { - "name": "moderation_message", - "ordinal": 24, - "type_info": "Varchar" - }, - { - "name": "moderation_message_body", - "ordinal": 25, - "type_info": "Varchar" - }, - { - "name": "client_side_type", - "ordinal": 26, - "type_info": "Varchar" - }, - { - "name": "server_side_type", - "ordinal": 27, - "type_info": "Varchar" - }, - { - "name": "project_type_name", - "ordinal": 28, - "type_info": "Varchar" - }, - { - "name": "webhook_sent", - "ordinal": 29, - "type_info": "Bool" - }, - { - "name": "color", - "ordinal": 30, - "type_info": "Int4" - }, - { - "name": "thread_id", - "ordinal": 31, - "type_info": "Int8" - }, - { - "name": "monetization_status", - "ordinal": 32, - "type_info": "Varchar" - }, - { - "name": "loaders", - "ordinal": 33, - "type_info": "VarcharArray" - }, - { - "name": "game_versions", - "ordinal": 34, - "type_info": "Jsonb" - }, - { - "name": "categories", - "ordinal": 35, - "type_info": "VarcharArray" - }, - { - "name": "additional_categories", - "ordinal": 36, - "type_info": "VarcharArray" - }, - { - "name": "versions", - "ordinal": 37, - "type_info": "Jsonb" - }, - { - "name": "gallery", - "ordinal": 38, - "type_info": "Jsonb" - }, - { - "name": "donations", - "ordinal": 39, - "type_info": "Jsonb" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - true, - false, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - false, - true, - true, - false, - null, - null, - null, - null, - null, - null, - null - ], - "parameters": { - "Left": [ - "Int8Array", - "TextArray", - "TextArray" - ] - } - }, - "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.webhook_sent, m.color,\n m.thread_id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n JSONB_AGG(DISTINCT jsonb_build_object('id', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,\n JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($3)\n LEFT JOIN loaders_versions lv ON v.id = lv.version_id\n LEFT JOIN loaders l ON lv.loader_id = l.id\n LEFT JOIN game_versions_versions gvv ON v.id = gvv.joining_version_id\n LEFT JOIN game_versions gv ON gvv.game_version_id = gv.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY pt.id, cs.id, ss.id, m.id;\n " - }, "83d428e1c07d16e356ef26bdf1d707940b1683b5f631ded1f6674a081453d67b": { "describe": { "columns": [], @@ -3469,6 +3133,19 @@ }, "query": "\n UPDATE users\n SET username = $1\n WHERE (id = $2)\n " }, + "86b5f8c13cf232d55a6f5053db2727036fd3ccc7bd31b32aa443993d4815ab8f": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Timestamptz", + "Int8" + ] + } + }, + "query": "\n UPDATE pats\n SET expires = $1\n WHERE id = $2\n " + }, "8abb317c85f48c7dd9ccf4a7b8fbc0b58ac73f7ae87ff2dfe67009a51089f784": { "describe": { "columns": [], @@ -3559,6 +3236,62 @@ }, "query": "\n DELETE FROM payouts_values\n WHERE user_id = $1\n " }, + "93c0fdb2bdc9c57602671d50108957654ede51e944944d4af59fe1ba1f6a336e": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "thread_type", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "mod_id", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "report_id", + "ordinal": 3, + "type_info": "Int8" + }, + { + "name": "show_in_mod_inbox", + "ordinal": 4, + "type_info": "Bool" + }, + { + "name": "members", + "ordinal": 5, + "type_info": "Int8Array" + }, + { + "name": "messages", + "ordinal": 6, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + true, + true, + false, + null, + null + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n SELECT t.id, t.thread_type, t.mod_id, t.report_id, t.show_in_mod_inbox,\n ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members,\n JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created)) filter (where tmsg.id is not null) messages\n FROM threads t\n LEFT OUTER JOIN threads_messages tmsg ON tmsg.thread_id = t.id\n LEFT OUTER JOIN threads_members tm ON tm.thread_id = t.id\n WHERE t.id = ANY($1)\n GROUP BY t.id\n " + }, "95c131d3ea36d53f9dccc6ff8bb7efd3fb571e4175857178c24f5c841a1ec7ed": { "describe": { "columns": [ @@ -3700,6 +3433,18 @@ }, "query": "\n SELECT id, name, email,\n avatar_url, username, bio,\n created, role, badges,\n balance, payout_wallet, payout_wallet_type, payout_address,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n " }, + "95cb791af4ea4d5b959de9e451bb8875336db33238024812086b5237b4dac350": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM pats WHERE id = $1\n " + }, "97690dda7edea8c985891cae5ad405f628ed81e333bc88df5493c928a4324d43": { "describe": { "columns": [ @@ -4123,32 +3868,6 @@ }, "query": "\n DELETE FROM states\n WHERE expires < CURRENT_DATE\n " }, - "a962f21969bba402258fca169c45f3d71bc1b71f754cdcc1f5c968e4948653b2": { - "describe": { - "columns": [ - { - "name": "notifs_count", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "followed_projects", - "ordinal": 1, - "type_info": "Int8Array" - } - ], - "nullable": [ - null, - null - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT COUNT(DISTINCT n.id) notifs_count, ARRAY_AGG(mf.mod_id) followed_projects FROM notifications n\n LEFT OUTER JOIN mod_follows mf ON mf.follower_id = $1\n WHERE user_id = $1 AND read = FALSE\n " - }, "aaec611bae08eac41c163367dc508208178170de91165095405f1b41e47f5e7f": { "describe": { "columns": [ @@ -4222,6 +3941,26 @@ }, "query": "\n DELETE FROM mods_donations\n WHERE joining_mod_id = $1\n " }, + "ad27195af9964c34803343c22abcb9aa6b52f2d1a370550ed4fb68bce2297e71": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "SELECT EXISTS(SELECT 1 FROM pats WHERE id=$1)" + }, "ae1686b8b566dd7ecc57c653c9313a4b324a2ec3a63aa6a44ed1d8ea7999b115": { "describe": { "columns": [], @@ -4259,7 +3998,45 @@ }, "query": "\n INSERT INTO threads_messages (\n id, author_id, body, thread_id\n )\n VALUES (\n $1, $2, $3, $4\n )\n " }, - "b0eb0c8e2c0f84bd7a7ba527b1dc6653e3d478a7be5e0a64d6965e8827c1e720": { + "b1e77dbaf4b190ab361f4fa203c442e5905cef6c1a135011a59ebd6e2dc0a92a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Numeric", + "Int8" + ] + } + }, + "query": "\n UPDATE users\n SET balance = balance - $1\n WHERE id = $2\n " + }, + "b297c97cd18785279cee369a1a269326ade765652ccf87405e6ee7dd3cbdaabf": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE pats\n SET name = $1\n WHERE id = $2\n " + }, + "b3345991457853c3f4c49dd68239bb23c3502d5c46008eb1b50233546a6ffa5d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n UPDATE payouts_values\n SET mod_id = NULL\n WHERE (mod_id = $1)\n " + }, + "b36877d60945eaae76680770a5d28d2cbb26cfbb0ec94ecc8f0741f48178ec1c": { "describe": { "columns": [], "nullable": [], @@ -4286,37 +4063,11 @@ "Text", "Int4", "Int4", - "Int8", "Varchar" ] } }, - "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, requested_status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type, color, thread_id, monetization_status\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13, $14,\n $15, $16, $17, $18,\n LOWER($19), $20, $21, $22, $23\n )\n " - }, - "b1e77dbaf4b190ab361f4fa203c442e5905cef6c1a135011a59ebd6e2dc0a92a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Numeric", - "Int8" - ] - } - }, - "query": "\n UPDATE users\n SET balance = balance - $1\n WHERE id = $2\n " - }, - "b3345991457853c3f4c49dd68239bb23c3502d5c46008eb1b50233546a6ffa5d": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE payouts_values\n SET mod_id = NULL\n WHERE (mod_id = $1)\n " + "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, requested_status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type, color, monetization_status\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13, $14,\n $15, $16, $17, $18,\n LOWER($19), $20, $21, $22\n )\n " }, "b903ac4e686ef85ba28d698c668da07860e7f276b261d8f2cebb74e73b094970": { "describe": { @@ -4429,27 +4180,6 @@ }, "query": "\n DELETE FROM dependencies WHERE dependent_id = $1\n " }, - "bea2ca01c4939a84b633927d81e5fd97d6bccf18f12a9cedc65719c33ef2c6b4": { - "describe": { - "columns": [ - { - "name": "exists", - "ordinal": 0, - "type_info": "Bool" - } - ], - "nullable": [ - null - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - } - }, - "query": "SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)" - }, "bee1abe8313d17a56d93b06a31240e338c3973bc7a7374799ced3df5e38d3134": { "describe": { "columns": [], @@ -4595,6 +4325,24 @@ }, "query": "\n DELETE FROM teams\n WHERE id = $1\n " }, + "c3f594d8d0ffcf5df1b36759cf3088bfaec496c5dfdbf496d3b05f0b122a5d0c": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int4", + "Int8", + "Int8", + "Int8", + "Varchar", + "Int8" + ] + } + }, + "query": "\n INSERT INTO reports (\n id, report_type_id, mod_id, version_id, user_id,\n body, reporter\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7\n )\n " + }, "c44e260a1f7712b14ac521fd301fea1b3f92238da62aeaf819997aecc365be43": { "describe": { "columns": [ @@ -4775,6 +4523,27 @@ }, "query": "\n UPDATE versions\n SET status = requested_status\n WHERE status = $1 AND date_published < CURRENT_DATE AND requested_status IS NOT NULL\n " }, + "c8fde56e5d03eda085519b4407768de7ddf48cae18ce7138a97e8e8fba967e15": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Int8Array", + "Int8" + ] + } + }, + "query": "\n SELECT id FROM reports\n WHERE id = ANY($1) AND reporter = $2\n " + }, "c94faba99d486b11509fff59465b7cc71983551b035e936ce4d9776510afb514": { "describe": { "columns": [ @@ -4868,6 +4637,262 @@ }, "query": "\n SELECT id, user_id, session, created, last_login, expires, refresh_expires, os, platform,\n city, country, ip, user_agent\n FROM sessions\n WHERE id = ANY($1) OR session = ANY($2)\n ORDER BY created DESC\n " }, + "cb1958b4d8821c8b1b3af4f567f7882939b0ac2fb50d12bd1638281d898b822f": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "project_type", + "ordinal": 1, + "type_info": "Int4" + }, + { + "name": "title", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "description", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "downloads", + "ordinal": 4, + "type_info": "Int4" + }, + { + "name": "follows", + "ordinal": 5, + "type_info": "Int4" + }, + { + "name": "icon_url", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "body", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "published", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "updated", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "approved", + "ordinal": 10, + "type_info": "Timestamptz" + }, + { + "name": "queued", + "ordinal": 11, + "type_info": "Timestamptz" + }, + { + "name": "status", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "requested_status", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "issues_url", + "ordinal": 14, + "type_info": "Varchar" + }, + { + "name": "source_url", + "ordinal": 15, + "type_info": "Varchar" + }, + { + "name": "wiki_url", + "ordinal": 16, + "type_info": "Varchar" + }, + { + "name": "discord_url", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "license_url", + "ordinal": 18, + "type_info": "Varchar" + }, + { + "name": "team_id", + "ordinal": 19, + "type_info": "Int8" + }, + { + "name": "client_side", + "ordinal": 20, + "type_info": "Int4" + }, + { + "name": "server_side", + "ordinal": 21, + "type_info": "Int4" + }, + { + "name": "license", + "ordinal": 22, + "type_info": "Varchar" + }, + { + "name": "slug", + "ordinal": 23, + "type_info": "Varchar" + }, + { + "name": "moderation_message", + "ordinal": 24, + "type_info": "Varchar" + }, + { + "name": "moderation_message_body", + "ordinal": 25, + "type_info": "Varchar" + }, + { + "name": "client_side_type", + "ordinal": 26, + "type_info": "Varchar" + }, + { + "name": "server_side_type", + "ordinal": 27, + "type_info": "Varchar" + }, + { + "name": "project_type_name", + "ordinal": 28, + "type_info": "Varchar" + }, + { + "name": "webhook_sent", + "ordinal": 29, + "type_info": "Bool" + }, + { + "name": "color", + "ordinal": 30, + "type_info": "Int4" + }, + { + "name": "thread_id", + "ordinal": 31, + "type_info": "Int8" + }, + { + "name": "monetization_status", + "ordinal": 32, + "type_info": "Varchar" + }, + { + "name": "loaders", + "ordinal": 33, + "type_info": "VarcharArray" + }, + { + "name": "game_versions", + "ordinal": 34, + "type_info": "Jsonb" + }, + { + "name": "categories", + "ordinal": 35, + "type_info": "VarcharArray" + }, + { + "name": "additional_categories", + "ordinal": 36, + "type_info": "VarcharArray" + }, + { + "name": "versions", + "ordinal": 37, + "type_info": "Jsonb" + }, + { + "name": "gallery", + "ordinal": 38, + "type_info": "Jsonb" + }, + { + "name": "donations", + "ordinal": 39, + "type_info": "Jsonb" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + true, + true, + false, + false, + false, + false, + true, + false, + false, + null, + null, + null, + null, + null, + null, + null + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray", + "TextArray" + ] + } + }, + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n JSONB_AGG(DISTINCT jsonb_build_object('id', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,\n JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($3)\n LEFT JOIN loaders_versions lv ON v.id = lv.version_id\n LEFT JOIN loaders l ON lv.loader_id = l.id\n LEFT JOIN game_versions_versions gvv ON v.id = gvv.joining_version_id\n LEFT JOIN game_versions gv ON gvv.game_version_id = gv.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY pt.id, cs.id, ss.id, t.id, m.id;\n " + }, "cb57ae673f1a7e50cc319efddb9bdc82e2251596bcf85aea52e8def343e423b8": { "describe": { "columns": [], @@ -5046,6 +5071,23 @@ }, "query": "\n UPDATE team_members\n SET accepted = TRUE\n WHERE (team_id = $1 AND user_id = $2)\n " }, + "d3d1467a5dcfc3eb34d7e821b0de54a419d9a5391c13254478944f2f2cc78fe6": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Varchar", + "Int8", + "Int8", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO pats (\n id, name, access_token, scopes, user_id,\n expires\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6\n )\n " + }, "d59a0ca4725d40232eae8bf5735787e1b76282c390d2a8d07fb34e237a0b2132": { "describe": { "columns": [], @@ -5437,6 +5479,69 @@ }, "query": "\n SELECT m.id id, tm.user_id user_id, tm.payouts_split payouts_split, pt.name project_type\n FROM mods m\n INNER JOIN team_members tm on m.team_id = tm.team_id AND tm.accepted = TRUE\n INNER JOIN project_types pt ON pt.id = m.project_type\n WHERE m.id = ANY($1) AND m.monetization_status = $2\n " }, + "e6f5a150cbd3bd6b9bde9e5cdad224a45c96d678b69ec12508e81246710e3f6d": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "access_token", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "scopes", + "ordinal": 3, + "type_info": "Int8" + }, + { + "name": "user_id", + "ordinal": 4, + "type_info": "Int8" + }, + { + "name": "created", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "expires", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "last_used", + "ordinal": 7, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray" + ] + } + }, + "query": "\n SELECT id, name, access_token, scopes, user_id, created, expires, last_used\n FROM pats\n WHERE id = ANY($1) OR access_token = ANY($2)\n ORDER BY created DESC\n " + }, "e7d0a64a08df6783c942f2fcadd94dd45f8d96ad3d3736e52ce90f68d396cdab": { "describe": { "columns": [ @@ -5637,6 +5742,19 @@ }, "query": "SELECT id FROM users WHERE discord_id = $1" }, + "eec6d4028d790e57a4d97fc5a200a9ae2b3d2cb60ee83c51fb05180b821558f5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "\n UPDATE pats\n SET scopes = $1\n WHERE id = $2\n " + }, "ef59f99fc0ab66ff5779d0e71c4a2134e2f26eed002ff9ea5626ea3e23518594": { "describe": { "columns": [ @@ -5751,27 +5869,6 @@ }, "query": "\n SELECT id, version_number, version_type\n FROM versions\n WHERE mod_id = $1 AND status = ANY($2)\n ORDER BY date_published ASC\n " }, - "f44572d8ef6ff10fb27a72233792f48cbf825bc58ecf1bc84dcc0aeeba3c12a0": { - "describe": { - "columns": [ - { - "name": "thread_id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8Array", - "Int8" - ] - } - }, - "query": "\n SELECT thread_id FROM reports\n WHERE thread_id = ANY($1) AND reporter = $2\n " - }, "f453b43772c4d2d9d09dc389eb95482cc75e7f0eaf9dc7ff48cf40f22f1497cc": { "describe": { "columns": [], diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 66d1c968c..fb6b67857 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,6 +1,6 @@ pub mod checks; pub mod flows; -pub mod pat; +pub mod pats; pub mod session; pub mod validate; diff --git a/src/auth/pat.rs b/src/auth/pat.rs deleted file mode 100644 index 4d47d0ff9..000000000 --- a/src/auth/pat.rs +++ /dev/null @@ -1,115 +0,0 @@ -// use crate::auth::AuthenticationError; -// use crate::database; -// use crate::database::models::{DatabaseError, UserId}; -// use crate::models::users::{self, Badges, RecipientType, RecipientWallet}; -// use censor::Censor; -// use chrono::{NaiveDateTime, Utc}; -// use rand::Rng; -// use serde::{Deserialize, Serialize}; -// -// #[derive(Serialize, Deserialize)] -// pub struct PersonalAccessToken { -// pub id: String, -// pub name: Option, -// pub access_token: Option, -// pub scope: i64, -// pub user_id: users::UserId, -// pub expires_at: NaiveDateTime, -// } -// // Find database user from PAT token -// // Separate to user_items as it may yet include further behaviour. -// pub async fn get_user_from_pat<'a, E>( -// access_token: &str, -// executor: E, -// ) -> Result, AuthenticationError> -// where -// E: sqlx::Executor<'a, Database = sqlx::Postgres>, -// { -// let row = sqlx::query!( -// " -// SELECT pats.expires_at, -// u.id, u.name, u.email, -// u.avatar_url, u.username, u.bio, -// u.created, u.role, u.badges, -// u.balance, u.payout_wallet, u.payout_wallet_type, u.payout_address, -// github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id -// FROM pats LEFT OUTER JOIN users u ON pats.user_id = u.id -// WHERE access_token = $1 -// ", -// access_token -// ) -// .fetch_optional(executor) -// .await?; -// if let Some(row) = row { -// if row.expires_at < Utc::now().naive_utc() { -// return Ok(None); -// } -// -// return Ok(Some(database::models::User { -// id: UserId(row.id), -// name: row.name, -// github_id: row.github_id, -// discord_id: row.discord_id, -// gitlab_id: row.gitlab_id, -// google_id: row.google_id, -// steam_id: row.steam_id, -// microsoft_id: row.microsoft_id, -// email: row.email, -// avatar_url: row.avatar_url, -// username: row.username, -// bio: row.bio, -// created: row.created, -// role: row.role, -// badges: Badges::from_bits(row.badges as u64).unwrap_or_default(), -// balance: row.balance, -// payout_wallet: row.payout_wallet.map(|x| RecipientWallet::from_string(&x)), -// payout_wallet_type: row -// .payout_wallet_type -// .map(|x| RecipientType::from_string(&x)), -// payout_address: row.payout_address, -// })); -// } -// Ok(None) -// } -// -// // Generate a new 128 char PAT token starting with 'modrinth_pat_' -// pub async fn generate_pat( -// con: &mut sqlx::Transaction<'_, sqlx::Postgres>, -// ) -> Result { -// let mut rng = rand::thread_rng(); -// let mut retry_count = 0; -// let censor = Censor::Standard + Censor::Sex; -// -// // First generate the PAT token as a random 128 char string. This may include uppercase and lowercase and numbers only. -// loop { -// let mut access_token = String::with_capacity(63); -// access_token.push_str("modrinth_pat_"); -// for _ in 0..51 { -// let c = rng.gen_range(0..62); -// if c < 10 { -// access_token.push(char::from_u32(c + 48).unwrap()); // 0-9 -// } else if c < 36 { -// access_token.push(char::from_u32(c + 55).unwrap()); // A-Z -// } else { -// access_token.push(char::from_u32(c + 61).unwrap()); // a-z -// } -// } -// let results = sqlx::query!( -// " -// SELECT EXISTS(SELECT 1 FROM pats WHERE access_token=$1) -// ", -// access_token -// ) -// .fetch_one(&mut *con) -// .await?; -// -// if !results.exists.unwrap_or(true) && !censor.check(&access_token) { -// break Ok(access_token); -// } -// -// retry_count += 1; -// if retry_count > 15 { -// return Err(DatabaseError::RandomId); -// } -// } -// } diff --git a/src/auth/pats.rs b/src/auth/pats.rs new file mode 100644 index 000000000..aaecb43d0 --- /dev/null +++ b/src/auth/pats.rs @@ -0,0 +1,269 @@ +use crate::database; +use crate::database::models::generate_pat_id; + +use crate::auth::get_user_from_headers; +use crate::routes::ApiError; + +use actix_web::web::{self, Data}; +use actix_web::{delete, get, patch, post, HttpRequest, HttpResponse}; +use chrono::{DateTime, Utc}; +use rand::distributions::Alphanumeric; +use rand::Rng; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; + +use crate::models::pats::{PersonalAccessToken, Scopes}; +use crate::queue::session::AuthQueue; +use crate::util::validate::validation_errors_to_string; +use serde::Deserialize; +use sqlx::postgres::PgPool; +use validator::Validate; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service(get_pats); + cfg.service(create_pat); + cfg.service(edit_pat); + cfg.service(delete_pat); +} + +#[get("pat")] +pub async fn get_pats( + req: HttpRequest, + pool: Data, + redis: Data, + session_queue: Data, +) -> Result { + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PAT_READ]), + ) + .await? + .1; + + let pat_ids = database::models::pat_item::PersonalAccessToken::get_user_pats( + user.id.into(), + &**pool, + &redis, + ) + .await?; + let pats = + database::models::pat_item::PersonalAccessToken::get_many_ids(&pat_ids, &**pool, &redis) + .await?; + + Ok(HttpResponse::Ok().json( + pats.into_iter() + .map(|x| PersonalAccessToken::from(x, false)) + .collect::>(), + )) +} + +#[derive(Deserialize, Validate)] +pub struct NewPersonalAccessToken { + pub scopes: Scopes, + #[validate(length(min = 3, max = 255))] + pub name: String, + pub expires: DateTime, +} + +#[post("pat")] +pub async fn create_pat( + req: HttpRequest, + info: web::Json, + pool: Data, + redis: Data, + session_queue: Data, +) -> Result { + info.0 + .validate() + .map_err(|err| ApiError::InvalidInput(validation_errors_to_string(err, None)))?; + + if info.scopes.restricted() { + return Err(ApiError::InvalidInput( + "Invalid scopes requested!".to_string(), + )); + } + if info.expires < Utc::now() { + return Err(ApiError::InvalidInput( + "Expire date must be in the future!".to_string(), + )); + } + + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PAT_CREATE]), + ) + .await? + .1; + + let mut transaction = pool.begin().await?; + + let id = generate_pat_id(&mut transaction).await?; + + let token = ChaCha20Rng::from_entropy() + .sample_iter(&Alphanumeric) + .take(60) + .map(char::from) + .collect::(); + let token = format!("mrp_{}", token); + + let name = info.name.clone(); + database::models::pat_item::PersonalAccessToken { + id, + name: name.clone(), + access_token: token.clone(), + scopes: info.scopes, + user_id: user.id.into(), + created: Utc::now(), + expires: info.expires, + last_used: None, + } + .insert(&mut transaction) + .await?; + + database::models::pat_item::PersonalAccessToken::clear_cache( + vec![(None, None, Some(user.id.into()))], + &redis, + ) + .await?; + transaction.commit().await?; + + Ok(HttpResponse::Ok().json(PersonalAccessToken { + id: id.into(), + name, + access_token: Some(token), + scopes: info.scopes, + user_id: user.id, + created: Utc::now(), + expires: info.expires, + last_used: None, + })) +} + +#[derive(Deserialize, Validate)] +pub struct ModifyPersonalAccessToken { + pub scopes: Option, + #[validate(length(min = 3, max = 255))] + pub name: Option, + pub expires: Option>, +} + +#[patch("pat/{id}")] +pub async fn edit_pat( + req: HttpRequest, + id: web::Path<(String,)>, + info: web::Json, + pool: Data, + redis: Data, + session_queue: Data, +) -> Result { + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PAT_WRITE]), + ) + .await? + .1; + + let id = id.into_inner().0; + let pat = database::models::pat_item::PersonalAccessToken::get(&id, &**pool, &redis).await?; + + if let Some(pat) = pat { + if pat.user_id == user.id.into() { + let mut transaction = pool.begin().await?; + + if let Some(scopes) = &info.scopes { + sqlx::query!( + " + UPDATE pats + SET scopes = $1 + WHERE id = $2 + ", + scopes.bits() as i64, + pat.id.0 + ) + .execute(&mut *transaction) + .await?; + } + if let Some(name) = &info.name { + sqlx::query!( + " + UPDATE pats + SET name = $1 + WHERE id = $2 + ", + name, + pat.id.0 + ) + .execute(&mut *transaction) + .await?; + } + if let Some(expires) = &info.expires { + sqlx::query!( + " + UPDATE pats + SET expires = $1 + WHERE id = $2 + ", + expires, + pat.id.0 + ) + .execute(&mut *transaction) + .await?; + } + + database::models::pat_item::PersonalAccessToken::clear_cache( + vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))], + &redis, + ) + .await?; + transaction.commit().await?; + } + } + + Ok(HttpResponse::NoContent().finish()) +} + +#[delete("pat/{id}")] +pub async fn delete_pat( + req: HttpRequest, + id: web::Path<(String,)>, + pool: Data, + redis: Data, + session_queue: Data, +) -> Result { + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PAT_DELETE]), + ) + .await? + .1; + let id = id.into_inner().0; + let pat = database::models::pat_item::PersonalAccessToken::get(&id, &**pool, &redis).await?; + + if let Some(pat) = pat { + if pat.user_id == user.id.into() { + let mut transaction = pool.begin().await?; + database::models::pat_item::PersonalAccessToken::remove(pat.id, &mut transaction) + .await?; + database::models::pat_item::PersonalAccessToken::clear_cache( + vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))], + &redis, + ) + .await?; + transaction.commit().await?; + } + } + + Ok(HttpResponse::NoContent().finish()) +} diff --git a/src/auth/session.rs b/src/auth/session.rs index 1d9512ede..27af05e1d 100644 --- a/src/auth/session.rs +++ b/src/auth/session.rs @@ -2,8 +2,9 @@ use crate::auth::{get_user_from_headers, AuthenticationError}; use crate::database::models::session_item::Session as DBSession; use crate::database::models::session_item::SessionBuilder; use crate::database::models::UserId; +use crate::models::pats::Scopes; use crate::models::sessions::Session; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::env::parse_var; use actix_web::http::header::AUTHORIZATION; @@ -122,9 +123,17 @@ pub async fn list( req: HttpRequest, pool: Data, redis: Data, - session_queue: Data, + session_queue: Data, ) -> Result { - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::SESSION_READ]), + ) + .await? + .1; let session_ids = DBSession::get_user_sessions(current_user.id.into(), &**pool, &redis).await?; let sessions = DBSession::get_many_ids(&session_ids, &**pool, &redis) @@ -143,9 +152,17 @@ pub async fn delete( req: HttpRequest, pool: Data, redis: Data, - session_queue: Data, + session_queue: Data, ) -> Result { - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::SESSION_DELETE]), + ) + .await? + .1; let session = DBSession::get(info.into_inner().0, &**pool, &redis).await?; @@ -174,9 +191,11 @@ pub async fn refresh( req: HttpRequest, pool: Data, redis: Data, - session_queue: Data, + session_queue: Data, ) -> Result { - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue, None) + .await? + .1; let session = req .headers() .get(AUTHORIZATION) diff --git a/src/auth/validate.rs b/src/auth/validate.rs index ef02e9b0f..1addb2341 100644 --- a/src/auth/validate.rs +++ b/src/auth/validate.rs @@ -2,8 +2,9 @@ use crate::auth::flows::AuthProvider; use crate::auth::session::get_session_metadata; use crate::auth::AuthenticationError; use crate::database::models::user_item; +use crate::models::pats::Scopes; use crate::models::users::{Role, User, UserId, UserPayoutData}; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use actix_web::HttpRequest; use chrono::Utc; use reqwest::header::{HeaderValue, AUTHORIZATION}; @@ -12,8 +13,9 @@ pub async fn get_user_from_headers<'a, E>( req: &HttpRequest, executor: E, redis: &deadpool_redis::Pool, - session_queue: &SessionQueue, -) -> Result + session_queue: &AuthQueue, + required_scopes: Option<&[Scopes]>, +) -> Result<(Scopes, User), AuthenticationError> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -21,7 +23,7 @@ where let token: Option<&HeaderValue> = headers.get(AUTHORIZATION); // Fetch DB user record and minos user from headers - let db_user = get_user_record_from_bearer_token( + let (scopes, db_user) = get_user_record_from_bearer_token( req, token .ok_or_else(|| AuthenticationError::InvalidAuthMethod)? @@ -57,7 +59,16 @@ where payout_address: db_user.payout_address, }), }; - Ok(user) + + if let Some(required_scopes) = required_scopes { + for scope in required_scopes { + if !scopes.contains(*scope) { + return Err(AuthenticationError::InvalidCredentials); + } + } + } + + Ok((scopes, user)) } pub async fn get_user_record_from_bearer_token<'a, 'b, E>( @@ -65,13 +76,28 @@ pub async fn get_user_record_from_bearer_token<'a, 'b, E>( token: &str, executor: E, redis: &deadpool_redis::Pool, - session_queue: &SessionQueue, -) -> Result, AuthenticationError> + session_queue: &AuthQueue, +) -> Result, AuthenticationError> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { let possible_user = match token.split_once('_') { - //Some(("modrinth", _)) => get_user_from_pat(token, executor).await?, + Some(("mrp", _)) => { + let pat = + crate::database::models::pat_item::PersonalAccessToken::get(token, executor, redis) + .await? + .ok_or_else(|| AuthenticationError::InvalidCredentials)?; + + if pat.expires < Utc::now() { + return Err(AuthenticationError::InvalidCredentials); + } + + let user = user_item::User::get_id(pat.user_id, executor, redis).await?; + + session_queue.add_pat(pat.id).await; + + user.map(|x| (pat.scopes, x)) + } Some(("mra", _)) => { let session = crate::database::models::session_item::Session::get(token, executor, redis) @@ -85,23 +111,31 @@ where let user = user_item::User::get_id(session.user_id, executor, redis).await?; let rate_limit_ignore = dotenvy::var("RATE_LIMIT_IGNORE_KEY")?; - if !req.headers().get("x-ratelimit-key").and_then(|x| x.to_str().ok()).map(|x| x == rate_limit_ignore).unwrap_or(false) { + if !req + .headers() + .get("x-ratelimit-key") + .and_then(|x| x.to_str().ok()) + .map(|x| x == rate_limit_ignore) + .unwrap_or(false) + { let metadata = get_session_metadata(req).await?; - session_queue.add(session.id, metadata).await; + session_queue.add_session(session.id, metadata).await; } - user + user.map(|x| (Scopes::ALL, x)) } Some(("github", _)) | Some(("gho", _)) | Some(("ghp", _)) => { let user = AuthProvider::GitHub.get_user(token).await?; let id = AuthProvider::GitHub.get_user_id(&user.id, executor).await?; - user_item::User::get_id( + let user = user_item::User::get_id( id.ok_or_else(|| AuthenticationError::InvalidCredentials)?, executor, redis, ) - .await? + .await?; + + user.map(|x| (Scopes::ALL, x)) } _ => return Err(AuthenticationError::InvalidAuthMethod), }; @@ -112,12 +146,14 @@ pub async fn check_is_moderator_from_headers<'a, 'b, E>( req: &HttpRequest, executor: E, redis: &deadpool_redis::Pool, - session_queue: &SessionQueue, + session_queue: &AuthQueue, ) -> Result where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { - let user = get_user_from_headers(req, executor, redis, session_queue).await?; + let user = get_user_from_headers(req, executor, redis, session_queue, None) + .await? + .1; if user.role.is_mod() { Ok(user) diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index d5e9abca9..481bf9bf0 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -83,13 +83,13 @@ generate_ids!( "SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)", StateId ); -// generate_ids!( -// pub generate_pat_id, -// PatId, -// 8, -// "SELECT EXISTS(SELECT 1 FROM pats WHERE id=$1)", -// PatId -// ); +generate_ids!( + pub generate_pat_id, + PatId, + 8, + "SELECT EXISTS(SELECT 1 FROM pats WHERE id=$1)", + PatId +); generate_ids!( pub generate_user_id, @@ -193,7 +193,7 @@ pub struct FileId(pub i64); #[sqlx(transparent)] pub struct StateId(pub i64); -#[derive(Copy, Clone, Debug, Type)] +#[derive(Copy, Clone, Debug, Type, Deserialize, Serialize)] #[sqlx(transparent)] pub struct PatId(pub i64); @@ -302,3 +302,8 @@ impl From for ids::SessionId { ids::SessionId(id.0 as u64) } } +impl From for ids::PatId { + fn from(id: PatId) -> Self { + ids::PatId(id.0 as u64) + } +} diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index fab0bc5f4..0aa8fbd15 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -3,6 +3,7 @@ use thiserror::Error; pub mod categories; pub mod ids; pub mod notification_item; +pub mod pat_item; pub mod project_item; pub mod report_item; pub mod session_item; diff --git a/src/database/models/pat_item.rs b/src/database/models/pat_item.rs new file mode 100644 index 000000000..822cfcb18 --- /dev/null +++ b/src/database/models/pat_item.rs @@ -0,0 +1,289 @@ +use super::ids::*; +use crate::database::models::DatabaseError; +use crate::models::ids::base62_impl::{parse_base62, to_base62}; +use crate::models::pats::Scopes; +use chrono::{DateTime, Utc}; +use redis::cmd; +use serde::{Deserialize, Serialize}; + +const PATS_NAMESPACE: &str = "pats"; +const PATS_TOKENS_NAMESPACE: &str = "pats_tokens"; +const PATS_USERS_NAMESPACE: &str = "pats_users"; +const DEFAULT_EXPIRY: i64 = 1800; // 30 minutes + +#[derive(Deserialize, Serialize)] +pub struct PersonalAccessToken { + pub id: PatId, + pub name: String, + pub access_token: String, + pub scopes: Scopes, + pub user_id: UserId, + pub created: DateTime, + pub expires: DateTime, + pub last_used: Option>, +} + +impl PersonalAccessToken { + pub async fn insert( + &self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result<(), DatabaseError> { + sqlx::query!( + " + INSERT INTO pats ( + id, name, access_token, scopes, user_id, + expires + ) + VALUES ( + $1, $2, $3, $4, $5, + $6 + ) + ", + self.id as PatId, + self.name, + self.access_token, + self.scopes.bits() as i64, + self.user_id as UserId, + self.expires + ) + .execute(&mut *transaction) + .await?; + + Ok(()) + } + + pub async fn get<'a, E, T: ToString>( + id: T, + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + Self::get_many(&[id], exec, redis) + .await + .map(|x| x.into_iter().next()) + } + + pub async fn get_many_ids<'a, E>( + pat_ids: &[PatId], + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let ids = pat_ids + .iter() + .map(|x| crate::models::ids::PatId::from(*x)) + .collect::>(); + PersonalAccessToken::get_many(&ids, exec, redis).await + } + + pub async fn get_many<'a, E, T: ToString>( + pat_strings: &[T], + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + use futures::TryStreamExt; + + if pat_strings.is_empty() { + return Ok(Vec::new()); + } + + let mut redis = redis.get().await?; + + let mut found_pats = Vec::new(); + let mut remaining_strings = pat_strings + .iter() + .map(|x| x.to_string()) + .collect::>(); + + let mut pat_ids = pat_strings + .iter() + .flat_map(|x| parse_base62(&x.to_string()).map(|x| x as i64)) + .collect::>(); + + pat_ids.append( + &mut cmd("MGET") + .arg( + pat_strings + .iter() + .map(|x| format!("{}:{}", PATS_TOKENS_NAMESPACE, x.to_string())) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await? + .into_iter() + .flatten() + .collect(), + ); + + if !pat_ids.is_empty() { + let pats = cmd("MGET") + .arg( + pat_ids + .iter() + .map(|x| format!("{}:{}", PATS_NAMESPACE, x)) + .collect::>(), + ) + .query_async::<_, Vec>>(&mut redis) + .await?; + + for pat in pats { + if let Some(pat) = + pat.and_then(|x| serde_json::from_str::(&x).ok()) + { + remaining_strings + .retain(|x| &to_base62(pat.id.0 as u64) != x && &pat.access_token != x); + found_pats.push(pat); + continue; + } + } + } + + if !remaining_strings.is_empty() { + let pat_ids_parsed: Vec = pat_strings + .iter() + .flat_map(|x| parse_base62(&x.to_string()).ok()) + .map(|x| x as i64) + .collect(); + let db_pats: Vec = sqlx::query!( + " + SELECT id, name, access_token, scopes, user_id, created, expires, last_used + FROM pats + WHERE id = ANY($1) OR access_token = ANY($2) + ORDER BY created DESC + ", + &pat_ids_parsed, + &pat_strings + .into_iter() + .map(|x| x.to_string()) + .collect::>(), + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|x| PersonalAccessToken { + id: PatId(x.id), + name: x.name, + access_token: x.access_token, + scopes: Scopes::from_bits(x.scopes as u64).unwrap_or(Scopes::NONE), + user_id: UserId(x.user_id), + created: x.created, + expires: x.expires, + last_used: x.last_used, + })) + }) + .try_collect::>() + .await?; + + for pat in db_pats { + cmd("SET") + .arg(format!("{}:{}", PATS_NAMESPACE, pat.id.0)) + .arg(serde_json::to_string(&pat)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + + cmd("SET") + .arg(format!("{}:{}", PATS_TOKENS_NAMESPACE, pat.access_token)) + .arg(pat.id.0) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + found_pats.push(pat); + } + } + + Ok(found_pats) + } + + pub async fn get_user_pats<'a, E>( + user_id: UserId, + exec: E, + redis: &deadpool_redis::Pool, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let mut redis = redis.get().await?; + let res = cmd("GET") + .arg(format!("{}:{}", PATS_USERS_NAMESPACE, user_id.0)) + .query_async::<_, Option>>(&mut redis) + .await?; + + if let Some(res) = res { + return Ok(res.into_iter().map(PatId).collect()); + } + + use futures::TryStreamExt; + let db_pats: Vec = sqlx::query!( + " + SELECT id + FROM pats + WHERE user_id = $1 + ORDER BY created DESC + ", + user_id.0, + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|x| PatId(x.id))) }) + .try_collect::>() + .await?; + + cmd("SET") + .arg(format!("{}:{}", PATS_USERS_NAMESPACE, user_id.0)) + .arg(serde_json::to_string(&db_pats)?) + .arg("EX") + .arg(DEFAULT_EXPIRY) + .query_async::<_, ()>(&mut redis) + .await?; + + Ok(db_pats) + } + + pub async fn clear_cache( + clear_pats: Vec<(Option, Option, Option)>, + redis: &deadpool_redis::Pool, + ) -> Result<(), DatabaseError> { + let mut redis = redis.get().await?; + let mut cmd = cmd("DEL"); + + for (id, token, user_id) in clear_pats { + if let Some(id) = id { + cmd.arg(format!("{}:{}", PATS_NAMESPACE, id.0)); + } + if let Some(token) = token { + cmd.arg(format!("{}:{}", PATS_TOKENS_NAMESPACE, token)); + } + if let Some(user_id) = user_id { + cmd.arg(format!("{}:{}", PATS_USERS_NAMESPACE, user_id.0)); + } + } + + cmd.query_async::<_, ()>(&mut redis).await?; + + Ok(()) + } + + pub async fn remove( + id: PatId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result, sqlx::error::Error> { + sqlx::query!( + " + DELETE FROM pats WHERE id = $1 + ", + id as PatId, + ) + .execute(&mut *transaction) + .await?; + + Ok(Some(())) + } +} diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 57c76a2c8..189448bae 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -85,6 +85,7 @@ impl GalleryItem { } } +#[derive(Clone)] pub struct ProjectBuilder { pub project_id: ProjectId, pub project_type_id: ProjectTypeId, @@ -110,7 +111,6 @@ pub struct ProjectBuilder { pub donation_urls: Vec, pub gallery_items: Vec, pub color: Option, - pub thread_id: ThreadId, pub monetization_status: MonetizationStatus, } @@ -153,7 +153,6 @@ impl ProjectBuilder { moderation_message_body: None, webhook_sent: false, color: self.color, - thread_id: Some(self.thread_id), monetization_status: self.monetization_status, }; project_struct.insert(&mut *transaction).await?; @@ -231,7 +230,6 @@ pub struct Project { pub moderation_message_body: Option, pub webhook_sent: bool, pub color: Option, - pub thread_id: Option, pub monetization_status: MonetizationStatus, } @@ -247,14 +245,14 @@ impl Project { published, downloads, icon_url, issues_url, source_url, wiki_url, status, requested_status, discord_url, client_side, server_side, license_url, license, - slug, project_type, color, thread_id, monetization_status + slug, project_type, color, monetization_status ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, - LOWER($19), $20, $21, $22, $23 + LOWER($19), $20, $21, $22 ) ", self.id as ProjectId, @@ -278,7 +276,6 @@ impl Project { self.slug.as_ref(), self.project_type as ProjectTypeId, self.color.map(|x| x as i32), - self.thread_id.map(|x| x.0), self.monetization_status.as_str(), ) .execute(&mut *transaction) @@ -381,6 +378,8 @@ impl Project { .execute(&mut *transaction) .await?; + models::Thread::remove_full(project.thread_id, transaction).await?; + sqlx::query!( " DELETE FROM mods @@ -413,10 +412,6 @@ impl Project { .execute(&mut *transaction) .await?; - if let Some(thread_id) = project.inner.thread_id { - models::Thread::remove_full(thread_id, transaction).await?; - } - Ok(Some(())) } else { Ok(None) @@ -551,7 +546,7 @@ impl Project { m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body, cs.name client_side_type, ss.name server_side_type, pt.name project_type_name, m.webhook_sent, m.color, - m.thread_id thread_id, m.monetization_status monetization_status, + t.id thread_id, m.monetization_status monetization_status, ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders, JSONB_AGG(DISTINCT jsonb_build_object('id', gv.version, 'created', gv.created)) filter (where gv.version is not null) game_versions, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories, @@ -563,6 +558,7 @@ impl Project { INNER JOIN project_types pt ON pt.id = m.project_type INNER JOIN side_types cs ON m.client_side = cs.id INNER JOIN side_types ss ON m.server_side = ss.id + INNER JOIN threads t ON t.mod_id = m.id LEFT JOIN mods_gallery mg ON mg.mod_id = m.id LEFT JOIN mods_donations md ON md.joining_mod_id = m.id LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id @@ -574,7 +570,7 @@ impl Project { LEFT JOIN game_versions_versions gvv ON v.id = gvv.joining_version_id LEFT JOIN game_versions gv ON gvv.game_version_id = gv.id WHERE m.id = ANY($1) OR m.slug = ANY($2) - GROUP BY pt.id, cs.id, ss.id, m.id; + GROUP BY pt.id, cs.id, ss.id, t.id, m.id; ", &project_ids_parsed, &remaining_strings.into_iter().map(|x| x.to_string().to_lowercase()).collect::>(), @@ -620,7 +616,6 @@ impl Project { webhook_sent: m.webhook_sent, color: m.color.map(|x| x as u32), queued: m.queued, - thread_id: m.thread_id.map(ThreadId), monetization_status: MonetizationStatus::from_str( &m.monetization_status, ), @@ -676,8 +671,9 @@ impl Project { game_versions.sort_by(|a, b| a.created.cmp(&b.created)); game_versions.into_iter().map(|x| x.id).collect() - } - }})) + }, + thread_id: ThreadId(m.thread_id), + }})) }) .try_collect::>() .await?; @@ -814,4 +810,5 @@ pub struct QueryProject { pub server_side: crate::models::projects::SideType, pub loaders: Vec, pub game_versions: Vec, + pub thread_id: ThreadId, } diff --git a/src/database/models/report_item.rs b/src/database/models/report_item.rs index bd352a740..89c9a44cb 100644 --- a/src/database/models/report_item.rs +++ b/src/database/models/report_item.rs @@ -11,7 +11,6 @@ pub struct Report { pub reporter: UserId, pub created: DateTime, pub closed: bool, - pub thread_id: ThreadId, } pub struct QueryReport { @@ -24,7 +23,7 @@ pub struct QueryReport { pub reporter: UserId, pub created: DateTime, pub closed: bool, - pub thread_id: Option, + pub thread_id: ThreadId, } impl Report { @@ -36,11 +35,11 @@ impl Report { " INSERT INTO reports ( id, report_type_id, mod_id, version_id, user_id, - body, reporter, thread_id + body, reporter ) VALUES ( $1, $2, $3, $4, $5, - $6, $7, $8 + $6, $7 ) ", self.id as ReportId, @@ -49,8 +48,7 @@ impl Report { self.version_id.map(|x| x.0 as i64), self.user_id.map(|x| x.0 as i64), self.body, - self.reporter as UserId, - self.thread_id as ThreadId, + self.reporter as UserId ) .execute(&mut *transaction) .await?; @@ -79,9 +77,10 @@ impl Report { let report_ids_parsed: Vec = report_ids.iter().map(|x| x.0).collect(); let reports = sqlx::query!( " - SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, r.thread_id, r.closed + SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, t.id thread_id, r.closed FROM reports r INNER JOIN report_types rt ON rt.id = r.report_type_id + INNER JOIN threads t ON t.report_id = r.id WHERE r.id = ANY($1) ORDER BY r.created DESC ", @@ -99,7 +98,7 @@ impl Report { reporter: UserId(x.reporter), created: x.created, closed: x.closed, - thread_id: x.thread_id.map(ThreadId), + thread_id: ThreadId(x.thread_id) })) }) .try_collect::>() @@ -127,14 +126,18 @@ impl Report { let thread_id = sqlx::query!( " - SELECT thread_id FROM reports - WHERE id = $1 + SELECT id FROM threads + WHERE report_id = $1 ", id as ReportId ) .fetch_optional(&mut *transaction) .await?; + if let Some(thread_id) = thread_id { + crate::database::models::Thread::remove_full(ThreadId(thread_id.id), transaction).await?; + } + sqlx::query!( " DELETE FROM reports WHERE id = $1 @@ -144,12 +147,6 @@ impl Report { .execute(&mut *transaction) .await?; - if let Some(thread_id) = thread_id { - if let Some(id) = thread_id.thread_id { - crate::database::models::Thread::remove_full(ThreadId(id), transaction).await?; - } - } - Ok(Some(())) } } diff --git a/src/database/models/session_item.rs b/src/database/models/session_item.rs index cfe607278..362404970 100644 --- a/src/database/models/session_item.rs +++ b/src/database/models/session_item.rs @@ -107,14 +107,14 @@ impl Session { } pub async fn get_many_ids<'a, E>( - user_ids: &[SessionId], + session_ids: &[SessionId], exec: E, redis: &deadpool_redis::Pool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { - let ids = user_ids + let ids = session_ids .iter() .map(|x| crate::models::ids::SessionId::from(*x)) .collect::>(); diff --git a/src/database/models/thread_item.rs b/src/database/models/thread_item.rs index 18c2a04c4..af6759c3e 100644 --- a/src/database/models/thread_item.rs +++ b/src/database/models/thread_item.rs @@ -7,12 +7,18 @@ use serde::Deserialize; pub struct ThreadBuilder { pub type_: ThreadType, pub members: Vec, + pub project_id: Option, + pub report_id: Option, } #[derive(Clone)] pub struct Thread { pub id: ThreadId, + + pub project_id: Option, + pub report_id: Option, pub type_: ThreadType, + pub messages: Vec, pub members: Vec, pub show_in_mod_inbox: bool, @@ -70,14 +76,16 @@ impl ThreadBuilder { sqlx::query!( " INSERT INTO threads ( - id, thread_type + id, thread_type, mod_id, report_id ) VALUES ( - $1, $2 + $1, $2, $3, $4 ) ", thread_id as ThreadId, - self.type_.as_str() + self.type_.as_str(), + self.project_id.map(|x| x.0), + self.report_id.map(|x| x.0), ) .execute(&mut *transaction) .await?; @@ -125,7 +133,7 @@ impl Thread { let thread_ids_parsed: Vec = thread_ids.iter().map(|x| x.0).collect(); let threads = sqlx::query!( " - SELECT t.id, t.thread_type, t.show_in_mod_inbox, + SELECT t.id, t.thread_type, t.mod_id, t.report_id, t.show_in_mod_inbox, ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members, JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created)) filter (where tmsg.id is not null) messages FROM threads t @@ -140,6 +148,8 @@ impl Thread { .try_filter_map(|e| async { Ok(e.right().map(|x| Thread { id: ThreadId(x.id), + project_id: x.mod_id.map(ProjectId), + report_id: x.report_id.map(ReportId), type_: ThreadType::from_str(&x.thread_type), messages: { let mut messages: Vec = serde_json::from_value( diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 11fb453b9..8847dab3d 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -10,11 +10,10 @@ use std::cmp::Ordering; use std::collections::HashMap; const VERSIONS_NAMESPACE: &str = "versions"; -// TODO: Cache version slugs call -// const VERSIONS_SLUGS_NAMESPACE: &str = "versions_slugs"; const VERSION_FILES_NAMESPACE: &str = "versions_files"; const DEFAULT_EXPIRY: i64 = 1800; // 30 minutes +#[derive(Clone)] pub struct VersionBuilder { pub version_id: VersionId, pub project_id: ProjectId, @@ -32,6 +31,7 @@ pub struct VersionBuilder { pub requested_status: Option, } +#[derive(Clone)] pub struct DependencyBuilder { pub project_id: Option, pub version_id: Option, @@ -79,6 +79,7 @@ impl DependencyBuilder { } } +#[derive(Clone)] pub struct VersionFileBuilder { pub url: String, pub filename: String, @@ -130,6 +131,7 @@ impl VersionFileBuilder { } } +#[derive(Clone)] pub struct HashBuilder { pub algorithm: String, pub hash: Vec, diff --git a/src/main.rs b/src/main.rs index ede962874..5c92498a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use crate::file_hosting::S3Host; use crate::queue::download::DownloadQueue; use crate::queue::payouts::PayoutsQueue; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::ratelimit::errors::ARError; use crate::ratelimit::memory::{MemoryStore, MemoryStoreActor}; use crate::ratelimit::middleware::RateLimiter; @@ -288,7 +288,7 @@ async fn main() -> std::io::Result<()> { } }); - let session_queue = web::Data::new(SessionQueue::new()); + let session_queue = web::Data::new(AuthQueue::new()); let pool_ref = pool.clone(); let redis_ref = redis_pool.clone(); diff --git a/src/models/ids.rs b/src/models/ids.rs index b5385bd59..b58d5926e 100644 --- a/src/models/ids.rs +++ b/src/models/ids.rs @@ -1,6 +1,7 @@ use thiserror::Error; pub use super::notifications::NotificationId; +pub use super::pats::PatId; pub use super::projects::{ProjectId, VersionId}; pub use super::reports::ReportId; pub use super::sessions::SessionId; @@ -115,6 +116,7 @@ base62_id_impl!(NotificationId, NotificationId); base62_id_impl!(ThreadId, ThreadId); base62_id_impl!(ThreadMessageId, ThreadMessageId); base62_id_impl!(SessionId, SessionId); +base62_id_impl!(PatId, PatId); pub mod base62_impl { use serde::de::{self, Deserializer, Visitor}; diff --git a/src/models/mod.rs b/src/models/mod.rs index d3f30e60b..dfe10c4b6 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -2,6 +2,7 @@ pub mod error; pub mod ids; pub mod notifications; pub mod pack; +pub mod pats; pub mod projects; pub mod reports; pub mod sessions; diff --git a/src/models/pats.rs b/src/models/pats.rs new file mode 100644 index 000000000..2cc8a8b43 --- /dev/null +++ b/src/models/pats.rs @@ -0,0 +1,137 @@ +use super::ids::Base62Id; +use crate::models::ids::UserId; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// The ID of a team +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "Base62Id")] +#[serde(into = "Base62Id")] +pub struct PatId(pub u64); + +bitflags::bitflags! { + #[derive(Serialize, Deserialize)] + #[serde(transparent)] + pub struct Scopes: u64 { + // read a user's email + const USER_READ_EMAIL = 1 << 0; + // read a user's data + const USER_READ = 1 << 1; + // write to a user's profile (edit username, email, avatar, follows, etc) + const USER_WRITE = 1 << 2; + // delete a user + const USER_DELETE = 1 << 3; + // modify a user's authentication data + const USER_AUTH_WRITE = 1 << 4; + + // read a user's notifications + const NOTIFICATION_READ = 1 << 5; + // delete or read a notification + const NOTIFICATION_WRITE = 1 << 6; + + // read a user's payouts data + const PAYOUTS_READ = 1 << 7; + // withdraw money from a user's account + const PAYOUTS_WRITE = 1<< 8; + // access user analytics (payout analytics at the moment) + const ANALYTICS = 1 << 9; + + // create a project + const PROJECT_CREATE = 1 << 10; + // read a user's projects (including private) + const PROJECT_READ = 1 << 11; + // write to a project's data (metadata, title, team members, etc) + const PROJECT_WRITE = 1 << 12; + // delete a project + const PROJECT_DELETE = 1 << 13; + + // create a version + const VERSION_CREATE = 1 << 14; + // read a user's versions (including private) + const VERSION_READ = 1 << 15; + // write to a version's data (metadata, files, etc) + const VERSION_WRITE = 1 << 16; + // delete a project + const VERSION_DELETE = 1 << 17; + + // create a report + const REPORT_CREATE = 1 << 18; + // read a user's reports + const REPORT_READ = 1 << 19; + // edit a report + const REPORT_WRITE = 1 << 20; + // delete a report + const REPORT_DELETE = 1 << 21; + + // read a thread + const THREAD_READ = 1 << 22; + // write to a thread (send a message, delete a message) + const THREAD_WRITE = 1 << 23; + + // create a pat + const PAT_CREATE = 1 << 24; + // read a user's pats + const PAT_READ = 1 << 25; + // edit a pat + const PAT_WRITE = 1 << 26; + // delete a pat + const PAT_DELETE = 1 << 27; + + // read a user's sessions + const SESSION_READ = 1 << 28; + // delete a session22 + const SESSION_DELETE = 1 << 29; + + const ALL = 0b11111111111111111111111111; + const NONE = 0b0; + } +} + +impl Scopes { + // these scopes cannot be specified in a personal access token + pub fn restricted(&self) -> bool { + self.contains( + Scopes::PAT_CREATE + | Scopes::PAT_READ + | Scopes::PAT_WRITE + | Scopes::PAT_DELETE + | Scopes::SESSION_READ + | Scopes::SESSION_DELETE + | Scopes::USER_AUTH_WRITE, + ) + } +} + +#[derive(Serialize, Deserialize)] +pub struct PersonalAccessToken { + pub id: PatId, + pub name: String, + pub access_token: Option, + pub scopes: Scopes, + pub user_id: UserId, + pub created: DateTime, + pub expires: DateTime, + pub last_used: Option>, +} + +impl PersonalAccessToken { + pub fn from( + data: crate::database::models::pat_item::PersonalAccessToken, + include_token: bool, + ) -> Self { + Self { + id: data.id.into(), + name: data.name, + access_token: if include_token { + Some(data.access_token) + } else { + None + }, + scopes: data.scopes, + user_id: data.user_id.into(), + created: data.created, + expires: data.expires, + last_used: data.last_used, + } + } +} diff --git a/src/models/projects.rs b/src/models/projects.rs index 4a95866aa..b10f6fdc0 100644 --- a/src/models/projects.rs +++ b/src/models/projects.rs @@ -106,7 +106,7 @@ pub struct Project { pub color: Option, /// The thread of the moderation messages of the project - pub thread_id: Option, + pub thread_id: ThreadId, /// The monetization status of this project pub monetization_status: MonetizationStatus, @@ -196,7 +196,7 @@ impl From for Project { }) .collect(), color: m.color, - thread_id: m.thread_id.map(|x| x.into()), + thread_id: data.thread_id.into(), monetization_status: m.monetization_status, } } diff --git a/src/models/reports.rs b/src/models/reports.rs index 5bd8e254b..b6393d610 100644 --- a/src/models/reports.rs +++ b/src/models/reports.rs @@ -19,7 +19,7 @@ pub struct Report { pub body: String, pub created: DateTime, pub closed: bool, - pub thread_id: Option, + pub thread_id: ThreadId, } #[derive(Serialize, Deserialize, Clone)] @@ -67,7 +67,7 @@ impl From for Report { body: x.body, created: x.created, closed: x.closed, - thread_id: x.thread_id.map(|x| x.into()), + thread_id: x.thread_id.into(), } } } diff --git a/src/models/teams.rs b/src/models/teams.rs index fa7d93a9c..346e25057 100644 --- a/src/models/teams.rs +++ b/src/models/teams.rs @@ -12,7 +12,6 @@ pub struct TeamId(pub u64); pub const OWNER_ROLE: &str = "Owner"; pub const DEFAULT_ROLE: &str = "Member"; -// TODO: permissions, role names, etc /// A team of users who control a project #[derive(Serialize, Deserialize)] pub struct Team { diff --git a/src/models/threads.rs b/src/models/threads.rs index 0f6ad9801..49ad6807a 100644 --- a/src/models/threads.rs +++ b/src/models/threads.rs @@ -3,6 +3,7 @@ use crate::models::projects::ProjectStatus; use crate::models::users::{User, UserId}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use crate::models::ids::{ProjectId, ReportId}; #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(from = "Base62Id")] @@ -19,6 +20,8 @@ pub struct Thread { pub id: ThreadId, #[serde(rename = "type")] pub type_: ThreadType, + pub project_id: Option, + pub report_id: Option, pub messages: Vec, pub members: Vec, } @@ -90,6 +93,8 @@ impl Thread { Thread { id: data.id.into(), type_: thread_type, + project_id: data.project_id.map(|x| x.into()), + report_id: data.report_id.map(|x| x.into()), messages: data .messages .into_iter() diff --git a/src/queue/session.rs b/src/queue/session.rs index dc71c22e6..b648101ee 100644 --- a/src/queue/session.rs +++ b/src/queue/session.rs @@ -1,27 +1,41 @@ use crate::auth::session::SessionMetadata; +use crate::database::models::pat_item::PersonalAccessToken; use crate::database::models::session_item::Session; -use crate::database::models::{DatabaseError, SessionId, UserId}; +use crate::database::models::{DatabaseError, PatId, SessionId, UserId}; use chrono::Utc; use sqlx::PgPool; use tokio::sync::Mutex; -pub struct SessionQueue { - queue: Mutex>, +pub struct AuthQueue { + session_queue: Mutex>, + pat_queue: Mutex>, } // Batches session accessing transactions every 30 seconds -impl SessionQueue { +impl AuthQueue { pub fn new() -> Self { - SessionQueue { - queue: Mutex::new(Vec::with_capacity(1000)), + AuthQueue { + session_queue: Mutex::new(Vec::with_capacity(1000)), + pat_queue: Mutex::new(Vec::with_capacity(1000)), } } - pub async fn add(&self, id: SessionId, metadata: SessionMetadata) { - self.queue.lock().await.push((id, metadata)); + pub async fn add_session(&self, id: SessionId, metadata: SessionMetadata) { + self.session_queue.lock().await.push((id, metadata)); } - pub async fn take(&self) -> Vec<(SessionId, SessionMetadata)> { - let mut queue = self.queue.lock().await; + pub async fn add_pat(&self, id: PatId) { + self.pat_queue.lock().await.push(id); + } + + pub async fn take_sessions(&self) -> Vec<(SessionId, SessionMetadata)> { + let mut queue = self.session_queue.lock().await; + let len = queue.len(); + + std::mem::replace(&mut queue, Vec::with_capacity(len)) + } + + pub async fn take_pats(&self) -> Vec { + let mut queue = self.pat_queue.lock().await; let len = queue.len(); std::mem::replace(&mut queue, Vec::with_capacity(len)) @@ -32,13 +46,14 @@ impl SessionQueue { pool: &PgPool, redis: &deadpool_redis::Pool, ) -> Result<(), DatabaseError> { - let queue = self.take().await; + let session_queue = self.take_sessions().await; + let pat_queue = self.take_pats().await; - if !queue.is_empty() { + if !session_queue.is_empty() || !pat_queue.is_empty() { let mut transaction = pool.begin().await?; let mut clear_cache_sessions = Vec::new(); - for (id, metadata) in queue { + for (id, metadata) in session_queue { clear_cache_sessions.push((Some(id), None, None)); sqlx::query!( @@ -83,6 +98,26 @@ impl SessionQueue { Session::clear_cache(clear_cache_sessions, redis).await?; + let mut clear_cache_pats = Vec::new(); + + for id in pat_queue { + clear_cache_pats.push((Some(id), None, None)); + + sqlx::query!( + " + UPDATE pats + SET last_used = $2 + WHERE (id = $1) + ", + id as PatId, + Utc::now(), + ) + .execute(&mut *transaction) + .await?; + } + + PersonalAccessToken::clear_cache(clear_cache_pats, redis).await?; + transaction.commit().await?; } diff --git a/src/routes/maven.rs b/src/routes/maven.rs index 465073377..f0850ec9d 100644 --- a/src/routes/maven.rs +++ b/src/routes/maven.rs @@ -1,8 +1,9 @@ use crate::auth::{get_user_from_headers, is_authorized_version}; use crate::database::models::project_item::QueryProject; use crate::database::models::version_item::{QueryFile, QueryVersion}; +use crate::models::pats::Scopes; use crate::models::projects::{ProjectId, VersionId}; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::{auth::is_authorized, database}; use actix_web::{get, route, web, HttpRequest, HttpResponse}; @@ -68,7 +69,7 @@ pub async fn maven_metadata( params: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let project_id = params.into_inner().0; let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?; @@ -79,9 +80,16 @@ pub async fn maven_metadata( return Ok(HttpResponse::NotFound().body("")); }; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); if !is_authorized(&data.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); @@ -190,7 +198,7 @@ pub async fn version_file( params: web::Path<(String, String, String)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let (project_id, vnum, file) = params.into_inner(); let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?; @@ -201,9 +209,16 @@ pub async fn version_file( return Ok(HttpResponse::NotFound().body("")); }; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); if !is_authorized(&project.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); @@ -274,7 +289,7 @@ pub async fn version_file_sha1( params: web::Path<(String, String, String)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let (project_id, vnum, file) = params.into_inner(); let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?; @@ -285,9 +300,16 @@ pub async fn version_file_sha1( return Ok(HttpResponse::NotFound().body("")); }; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); if !is_authorized(&project.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); @@ -332,7 +354,7 @@ pub async fn version_file_sha512( params: web::Path<(String, String, String)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let (project_id, vnum, file) = params.into_inner(); let project_data = database::models::Project::get(&project_id, &**pool, &redis).await?; @@ -343,9 +365,16 @@ pub async fn version_file_sha512( return Ok(HttpResponse::NotFound().body("")); }; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); if !is_authorized(&project.inner, &user_option, &pool).await? { return Ok(HttpResponse::NotFound().body("")); diff --git a/src/routes/updates.rs b/src/routes/updates.rs index d127f7403..13c8e3972 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -6,8 +6,9 @@ use sqlx::PgPool; use crate::auth::{filter_authorized_versions, get_user_from_headers, is_authorized}; use crate::database; +use crate::models::pats::Scopes; use crate::models::projects::VersionType; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use super::ApiError; @@ -21,7 +22,7 @@ pub async fn forge_updates( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { const ERROR: &str = "The specified project does not exist!"; @@ -31,9 +32,16 @@ pub async fn forge_updates( .await? .ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); if !is_authorized(&project.inner, &user_option, &pool).await? { return Err(ApiError::InvalidInput(ERROR.to_string())); diff --git a/src/routes/v2/mod.rs b/src/routes/v2/mod.rs index 96848d68f..cd28ee6c6 100644 --- a/src/routes/v2/mod.rs +++ b/src/routes/v2/mod.rs @@ -1,7 +1,6 @@ mod admin; mod moderation; mod notifications; -mod pats; pub(crate) mod project_creation; mod projects; mod reports; @@ -22,6 +21,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) { .configure(admin::config) .configure(crate::auth::session::config) .configure(crate::auth::flows::config) + .configure(crate::auth::pats::config) .configure(moderation::config) .configure(notifications::config) //.configure(pats::config) diff --git a/src/routes/v2/moderation.rs b/src/routes/v2/moderation.rs index b0f6da1d1..e1d6e995a 100644 --- a/src/routes/v2/moderation.rs +++ b/src/routes/v2/moderation.rs @@ -2,7 +2,7 @@ use super::ApiError; use crate::auth::check_is_moderator_from_headers; use crate::database; use crate::models::projects::ProjectStatus; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use actix_web::{get, web, HttpRequest, HttpResponse}; use serde::Deserialize; use sqlx::PgPool; @@ -27,7 +27,7 @@ pub async fn get_projects( pool: web::Data, redis: web::Data, count: web::Query, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { check_is_moderator_from_headers(&req, &**pool, &redis, &session_queue).await?; diff --git a/src/routes/v2/notifications.rs b/src/routes/v2/notifications.rs index 4bc23c79d..b0a0940d4 100644 --- a/src/routes/v2/notifications.rs +++ b/src/routes/v2/notifications.rs @@ -2,7 +2,8 @@ use crate::auth::get_user_from_headers; use crate::database; use crate::models::ids::NotificationId; use crate::models::notifications::Notification; -use crate::queue::session::SessionQueue; +use crate::models::pats::Scopes; +use crate::queue::session::AuthQueue; use crate::routes::ApiError; use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; @@ -32,9 +33,17 @@ pub async fn notifications_get( web::Query(ids): web::Query, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::NOTIFICATION_READ]), + ) + .await? + .1; use database::models::notification_item::Notification as DBNotification; use database::models::NotificationId as DBNotificationId; @@ -64,9 +73,17 @@ pub async fn notification_get( info: web::Path<(NotificationId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::NOTIFICATION_READ]), + ) + .await? + .1; let id = info.into_inner().0; @@ -90,9 +107,17 @@ pub async fn notification_read( info: web::Path<(NotificationId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::NOTIFICATION_WRITE]), + ) + .await? + .1; let id = info.into_inner().0; @@ -125,9 +150,17 @@ pub async fn notification_delete( info: web::Path<(NotificationId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::NOTIFICATION_WRITE]), + ) + .await? + .1; let id = info.into_inner().0; @@ -160,9 +193,17 @@ pub async fn notifications_read( web::Query(ids): web::Query, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::NOTIFICATION_WRITE]), + ) + .await? + .1; let notification_ids = serde_json::from_str::>(&ids.ids)? .into_iter() @@ -197,9 +238,17 @@ pub async fn notifications_delete( web::Query(ids): web::Query, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::NOTIFICATION_WRITE]), + ) + .await? + .1; let notification_ids = serde_json::from_str::>(&ids.ids)? .into_iter() diff --git a/src/routes/v2/pats.rs b/src/routes/v2/pats.rs deleted file mode 100644 index 2fb49be1b..000000000 --- a/src/routes/v2/pats.rs +++ /dev/null @@ -1,249 +0,0 @@ -// /*! -// Current edition of Ory kratos does not support PAT access of data, so this module is how we allow for PAT authentication. -// -// -// Just as a summary: Don't implement this flow in your application! -// */ -// -// use crate::database; -// use crate::database::models::generate_pat_id; -// use crate::models::ids::base62_impl::{parse_base62, to_base62}; -// -// use crate::auth::get_user_from_headers; -// use crate::auth::{generate_pat, PersonalAccessToken}; -// use crate::models::users::UserId; -// use crate::routes::ApiError; -// -// use actix_web::web::{self, Data, Query}; -// use actix_web::{delete, get, patch, post, HttpRequest, HttpResponse}; -// use chrono::{Duration, Utc}; -// -// use crate::queue::session::SessionQueue; -// use serde::Deserialize; -// use sqlx::postgres::PgPool; -// -// pub fn config(cfg: &mut web::ServiceConfig) { -// cfg.service(get_pats); -// cfg.service(create_pat); -// cfg.service(edit_pat); -// cfg.service(delete_pat); -// } -// -// #[derive(Deserialize)] -// pub struct CreatePersonalAccessToken { -// pub scope: i64, // todo: should be a vec of enum -// pub name: Option, -// pub expire_in_days: i64, // resets expiry to expire_in_days days from now -// } -// -// #[derive(Deserialize)] -// pub struct ModifyPersonalAccessToken { -// #[serde(default, with = "::serde_with::rust::double_option")] -// pub name: Option>, -// pub expire_in_days: Option, // resets expiry to expire_in_days days from now -// } -// -// // GET /pat -// // Get all personal access tokens for the given user. Minos/Kratos cookie must be attached for it to work. -// // Does not return the actual access token, only the ID + metadata. -// #[get("pat")] -// pub async fn get_pats( -// req: HttpRequest, -// pool: Data, -// redis: Data, -// session_queue: web::Data, -// ) -> Result { -// let user: crate::models::users::User = -// get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; -// let db_user_id: database::models::UserId = database::models::UserId::from(user.id); -// -// let pats = sqlx::query!( -// " -// SELECT id, name, user_id, scope, expires_at -// FROM pats -// WHERE user_id = $1 -// ", -// db_user_id.0 -// ) -// .fetch_all(&**pool) -// .await?; -// -// let pats = pats -// .into_iter() -// .map(|pat| PersonalAccessToken { -// id: to_base62(pat.id as u64), -// scope: pat.scope, -// name: pat.name, -// expires_at: pat.expires_at, -// access_token: None, -// user_id: UserId(pat.user_id as u64), -// }) -// .collect::>(); -// -// Ok(HttpResponse::Ok().json(pats)) -// } -// -// // POST /pat -// // Create a new personal access token for the given user. Minos/Kratos cookie must be attached for it to work. -// // All PAT tokens are base62 encoded, and are prefixed with "modrinth_pat_" -// #[post("pat")] -// pub async fn create_pat( -// req: HttpRequest, -// Query(info): Query, // callback url -// pool: Data, -// redis: web::Data, -// session_queue: web::Data, -// ) -> Result { -// let user: crate::models::users::User = -// get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; -// let db_user_id: database::models::UserId = database::models::UserId::from(user.id); -// -// let mut transaction: sqlx::Transaction = pool.begin().await?; -// -// let pat = generate_pat_id(&mut transaction).await?; -// let access_token = generate_pat(&mut transaction).await?; -// let expiry = Utc::now().naive_utc() + Duration::days(info.expire_in_days); -// if info.expire_in_days <= 0 { -// return Err(ApiError::InvalidInput( -// "'expire_in_days' must be greater than 0".to_string(), -// )); -// } -// -// sqlx::query!( -// " -// INSERT INTO pats (id, name, access_token, user_id, scope, expires_at) -// VALUES ($1, $2, $3, $4, $5, $6) -// ", -// pat.0, -// info.name, -// access_token, -// db_user_id.0, -// info.scope, -// expiry -// ) -// .execute(&mut *transaction) -// .await?; -// -// transaction.commit().await?; -// -// Ok(HttpResponse::Ok().json(PersonalAccessToken { -// id: to_base62(pat.0 as u64), -// access_token: Some(access_token), -// name: info.name, -// scope: info.scope, -// user_id: user.id, -// expires_at: expiry, -// })) -// } -// -// // PATCH /pat/(id) -// // Edit an access token of id "id" for the given user. -// // 'None' will mean not edited. Minos/Kratos cookie or PAT must be attached for it to work. -// #[patch("pat/{id}")] -// pub async fn edit_pat( -// req: HttpRequest, -// id: web::Path, -// Query(info): Query, // callback url -// pool: Data, -// redis: web::Data, -// session_queue: web::Data, -// ) -> Result { -// let user: crate::models::users::User = -// get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; -// let pat_id = database::models::PatId(parse_base62(&id)? as i64); -// let db_user_id: database::models::UserId = database::models::UserId::from(user.id); -// -// if let Some(expire_in_days) = info.expire_in_days { -// if expire_in_days <= 0 { -// return Err(ApiError::InvalidInput( -// "'expire_in_days' must be greater than 0".to_string(), -// )); -// } -// } -// -// // Get the singular PAT and user combination (failing immediately if it doesn't exist) -// let mut transaction = pool.begin().await?; -// let row = sqlx::query!( -// " -// SELECT id, name, scope, user_id, expires_at FROM pats -// WHERE id = $1 AND user_id = $2 -// ", -// pat_id.0, -// db_user_id.0 // included for safety -// ) -// .fetch_one(&**pool) -// .await?; -// -// let pat = PersonalAccessToken { -// id: to_base62(row.id as u64), -// access_token: None, -// user_id: UserId::from(db_user_id), -// name: info.name.unwrap_or(row.name), -// scope: row.scope, -// expires_at: info -// .expire_in_days -// .map(|d| Utc::now().naive_utc() + Duration::days(d)) -// .unwrap_or(row.expires_at), -// }; -// -// sqlx::query!( -// " -// UPDATE pats SET -// name = $1, -// expires_at = $2 -// WHERE id = $3 -// ", -// pat.name, -// pat.expires_at, -// parse_base62(&pat.id)? as i64 -// ) -// .execute(&mut *transaction) -// .await?; -// transaction.commit().await?; -// -// Ok(HttpResponse::Ok().json(pat)) -// } -// -// // DELETE /pat -// // Delete a personal access token for the given user. Minos/Kratos cookie must be attached for it to work. -// #[delete("pat/{id}")] -// pub async fn delete_pat( -// req: HttpRequest, -// id: web::Path, -// pool: Data, -// redis: web::Data, -// session_queue: web::Data, -// ) -> Result { -// let user: crate::models::users::User = -// get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; -// let pat_id = database::models::PatId(parse_base62(&id)? as i64); -// let db_user_id: database::models::UserId = database::models::UserId::from(user.id); -// -// // Get the singular PAT and user combination (failing immediately if it doesn't exist) -// // This is to prevent users from deleting other users' PATs -// let pat_id = sqlx::query!( -// " -// SELECT id FROM pats -// WHERE id = $1 AND user_id = $2 -// ", -// pat_id.0, -// db_user_id.0 -// ) -// .fetch_one(&**pool) -// .await? -// .id; -// -// let mut transaction = pool.begin().await?; -// sqlx::query!( -// " -// DELETE FROM pats -// WHERE id = $1 -// ", -// pat_id, -// ) -// .execute(&mut *transaction) -// .await?; -// transaction.commit().await?; -// -// Ok(HttpResponse::Ok().finish()) -// } diff --git a/src/routes/v2/project_creation.rs b/src/routes/v2/project_creation.rs index 3e3f6eb1b..87af9385c 100644 --- a/src/routes/v2/project_creation.rs +++ b/src/routes/v2/project_creation.rs @@ -4,13 +4,14 @@ use crate::database::models; use crate::database::models::thread_item::ThreadBuilder; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; +use crate::models::pats::Scopes; use crate::models::projects::{ DonationLink, License, MonetizationStatus, ProjectId, ProjectStatus, SideType, VersionId, VersionStatus, }; use crate::models::threads::ThreadType; use crate::models::users::UserId; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::search::indexing::IndexingError; use crate::util::routes::read_from_field; use crate::util::validate::validation_errors_to_string; @@ -273,7 +274,7 @@ pub async fn project_create( client: Data, redis: Data, file_host: Data>, - session_queue: Data, + session_queue: Data, ) -> Result { let mut transaction = client.begin().await?; let mut uploaded_files = Vec::new(); @@ -343,13 +344,21 @@ async fn project_create_inner( uploaded_files: &mut Vec, pool: &PgPool, redis: &deadpool_redis::Pool, - session_queue: &SessionQueue, + session_queue: &AuthQueue, ) -> Result { // The base URL for files uploaded to backblaze let cdn_url = dotenvy::var("CDN_URL")?; // The currently logged in user - let current_user = get_user_from_headers(&req, pool, redis, session_queue).await?; + let current_user = get_user_from_headers( + &req, + pool, + redis, + session_queue, + Some(&[Scopes::PROJECT_CREATE]), + ) + .await? + .1; let project_id: ProjectId = models::generate_project_id(transaction).await?.into(); @@ -726,14 +735,7 @@ async fn project_create_inner( } } - let thread_id = ThreadBuilder { - type_: ThreadType::Project, - members: vec![], - } - .insert(&mut *transaction) - .await?; - - let project_builder = models::project_item::ProjectBuilder { + let project_builder_actual = models::project_item::ProjectBuilder { project_id: project_id.into(), project_type_id, team_id, @@ -769,12 +771,23 @@ async fn project_create_inner( }) .collect(), color: icon_data.and_then(|x| x.1), - thread_id, monetization_status: MonetizationStatus::Monetized, }; + let project_builder = project_builder_actual.clone(); let now = Utc::now(); + let id = project_builder_actual.insert(&mut *transaction).await?; + + let thread_id = ThreadBuilder { + type_: ThreadType::Project, + members: vec![], + project_id: Some(id), + report_id: None, + } + .insert(&mut *transaction) + .await?; + let response = crate::models::projects::Project { id: project_id, slug: project_builder.slug.clone(), @@ -817,12 +830,10 @@ async fn project_create_inner( donation_urls: project_create_data.donation_urls.clone(), gallery: gallery_urls, color: project_builder.color, - thread_id: Some(project_builder.thread_id.into()), - monetization_status: project_builder.monetization_status, + thread_id: thread_id.into(), + monetization_status: MonetizationStatus::Monetized, }; - let _project_id = project_builder.insert(&mut *transaction).await?; - if status == ProjectStatus::Processing { if let Ok(webhook_url) = dotenvy::var("MODERATION_DISCORD_WEBHOOK") { crate::util::webhook::send_discord_webhook(response.id, pool, webhook_url, None) diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index 3f74efc9d..f4aa20757 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -6,12 +6,13 @@ use crate::file_hosting::FileHost; use crate::models; use crate::models::ids::base62_impl::parse_base62; use crate::models::notifications::NotificationBody; +use crate::models::pats::Scopes; use crate::models::projects::{ DonationLink, MonetizationStatus, Project, ProjectId, ProjectStatus, SearchRequest, SideType, }; use crate::models::teams::Permissions; use crate::models::threads::MessageBody; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::search::{search_for_project, SearchConfig, SearchError}; use crate::util::routes::read_from_payload; @@ -116,14 +117,21 @@ pub async fn projects_get( web::Query(ids): web::Query, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let ids = serde_json::from_str::>(&ids.ids)?; let projects_data = database::models::Project::get_many(&ids, &**pool, &redis).await?; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); let projects = filter_authorized_projects(projects_data, &user_option, &pool).await?; @@ -136,15 +144,22 @@ pub async fn project_get( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let string = info.into_inner().0; let project_data = database::models::Project::get(&string, &**pool, &redis).await?; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); if let Some(data) = project_data { if is_authorized(&data.inner, &user_option, &pool).await? { @@ -186,15 +201,22 @@ pub async fn dependency_list( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let string = info.into_inner().0; let result = database::models::Project::get(&string, &**pool, &redis).await?; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); if let Some(project) = result { if !is_authorized(&project.inner, &user_option, &pool).await? { @@ -357,9 +379,17 @@ pub async fn project_edit( config: web::Data, new_project: web::Json, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; new_project .validate() @@ -546,18 +576,16 @@ pub async fn project_edit( .await?; } - if let Some(thread) = project_item.inner.thread_id { - ThreadMessageBuilder { - author_id: Some(user.id.into()), - body: MessageBody::StatusChange { - new_status: *status, - old_status: project_item.inner.status, - }, - thread_id: thread, - } + ThreadMessageBuilder { + author_id: Some(user.id.into()), + body: MessageBody::StatusChange { + new_status: *status, + old_status: project_item.inner.status, + }, + thread_id: project_item.thread_id, + } .insert(&mut transaction) .await?; - } sqlx::query!( " @@ -1181,9 +1209,17 @@ pub async fn projects_edit( pool: web::Data, bulk_edit_project: web::Json, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; bulk_edit_project .validate() @@ -1511,10 +1547,18 @@ pub async fn project_schedule( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, scheduling_data: web::Json, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; if scheduling_data.time < Utc::now() { return Err(ApiError::InvalidInput( @@ -1591,11 +1635,19 @@ pub async fn project_icon_edit( redis: web::Data, file_host: web::Data>, mut payload: web::Payload, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) { let cdn_url = dotenvy::var("CDN_URL")?; - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; let string = info.into_inner().0; let project_item = database::models::Project::get(&string, &**pool, &redis) @@ -1687,9 +1739,17 @@ pub async fn delete_project_icon( pool: web::Data, redis: web::Data, file_host: web::Data>, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; let string = info.into_inner().0; let project_item = database::models::Project::get(&string, &**pool, &redis) @@ -1773,14 +1833,22 @@ pub async fn add_gallery_item( redis: web::Data, file_host: web::Data>, mut payload: web::Payload, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) { item.validate() .map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?; let cdn_url = dotenvy::var("CDN_URL")?; - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; let string = info.into_inner().0; let project_item = database::models::Project::get(&string, &**pool, &redis) @@ -1915,9 +1983,17 @@ pub async fn edit_gallery_item( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; let string = info.into_inner().0; item.validate() @@ -2061,9 +2137,17 @@ pub async fn delete_gallery_item( pool: web::Data, redis: web::Data, file_host: web::Data>, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; let string = info.into_inner().0; let project_item = database::models::Project::get(&string, &**pool, &redis) @@ -2148,9 +2232,17 @@ pub async fn project_delete( pool: web::Data, redis: web::Data, config: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_DELETE]), + ) + .await? + .1; let string = info.into_inner().0; let project = database::models::Project::get(&string, &**pool, &redis) @@ -2203,9 +2295,17 @@ pub async fn project_follow( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::USER_WRITE]), + ) + .await? + .1; let string = info.into_inner().0; let result = database::models::Project::get(&string, &**pool, &redis) @@ -2274,9 +2374,17 @@ pub async fn project_unfollow( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::USER_WRITE]), + ) + .await? + .1; let string = info.into_inner().0; let result = database::models::Project::get(&string, &**pool, &redis) diff --git a/src/routes/v2/reports.rs b/src/routes/v2/reports.rs index c93b528d1..451e425b2 100644 --- a/src/routes/v2/reports.rs +++ b/src/routes/v2/reports.rs @@ -1,9 +1,10 @@ use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; use crate::database::models::thread_item::{ThreadBuilder, ThreadMessageBuilder}; use crate::models::ids::{base62_impl::parse_base62, ProjectId, UserId, VersionId}; +use crate::models::pats::Scopes; use crate::models::reports::{ItemType, Report}; use crate::models::threads::{MessageBody, ThreadType}; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::routes::ApiError; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use chrono::Utc; @@ -35,11 +36,19 @@ pub async fn report_create( pool: web::Data, mut body: web::Payload, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let mut transaction = pool.begin().await?; - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::REPORT_CREATE]), + ) + .await? + .1; let mut bytes = web::BytesMut::new(); while let Some(item) = body.next().await { @@ -59,13 +68,6 @@ pub async fn report_create( ApiError::InvalidInput(format!("Invalid report type: {}", new_report.report_type)) })?; - let thread_id = ThreadBuilder { - type_: ThreadType::Report, - members: vec![], - } - .insert(&mut transaction) - .await?; - let mut report = crate::database::models::report_item::Report { id, report_type_id: report_type, @@ -76,7 +78,6 @@ pub async fn report_create( reporter: current_user.id.into(), created: Utc::now(), closed: false, - thread_id, }; match new_report.item_type { @@ -146,6 +147,15 @@ pub async fn report_create( } report.insert(&mut transaction).await?; + let thread_id = ThreadBuilder { + type_: ThreadType::Report, + members: vec![], + project_id: None, + report_id: Some(report.id), + } + .insert(&mut transaction) + .await?; + transaction.commit().await?; Ok(HttpResponse::Ok().json(Report { @@ -157,7 +167,7 @@ pub async fn report_create( body: new_report.body.clone(), created: Utc::now(), closed: false, - thread_id: Some(report.thread_id.into()), + thread_id: thread_id.into(), })) } @@ -182,9 +192,17 @@ pub async fn reports( pool: web::Data, redis: web::Data, count: web::Query, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::REPORT_READ]), + ) + .await? + .1; use futures::stream::TryStreamExt; @@ -248,7 +266,7 @@ pub async fn reports_get( web::Query(ids): web::Query, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let report_ids: Vec = serde_json::from_str::>(&ids.ids)? @@ -259,7 +277,15 @@ pub async fn reports_get( let reports_data = crate::database::models::report_item::Report::get_many(&report_ids, &**pool).await?; - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::REPORT_READ]), + ) + .await? + .1; let all_reports = reports_data .into_iter() @@ -276,9 +302,17 @@ pub async fn report_get( pool: web::Data, redis: web::Data, info: web::Path<(crate::models::reports::ReportId,)>, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::REPORT_READ]), + ) + .await? + .1; let id = info.into_inner().0.into(); let report = crate::database::models::report_item::Report::get(id, &**pool).await?; @@ -308,10 +342,18 @@ pub async fn report_edit( pool: web::Data, redis: web::Data, info: web::Path<(crate::models::reports::ReportId,)>, - session_queue: web::Data, + session_queue: web::Data, edit_report: web::Json, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::REPORT_WRITE]), + ) + .await? + .1; let id = info.into_inner().0.into(); let report = crate::database::models::report_item::Report::get(id, &**pool).await?; @@ -344,19 +386,17 @@ pub async fn report_edit( )); } - if let Some(thread) = report.thread_id { - ThreadMessageBuilder { - author_id: Some(user.id.into()), - body: if !edit_closed && report.closed { - MessageBody::ThreadReopen - } else { - MessageBody::ThreadClosure - }, - thread_id: thread, - } + ThreadMessageBuilder { + author_id: Some(user.id.into()), + body: if !edit_closed && report.closed { + MessageBody::ThreadReopen + } else { + MessageBody::ThreadClosure + }, + thread_id: report.thread_id, + } .insert(&mut transaction) .await?; - } sqlx::query!( " @@ -385,7 +425,7 @@ pub async fn report_delete( pool: web::Data, info: web::Path<(crate::models::reports::ReportId,)>, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { check_is_moderator_from_headers(&req, &**pool, &redis, &session_queue).await?; diff --git a/src/routes/v2/tags.rs b/src/routes/v2/tags.rs index 1b0fe2bbf..2a38954be 100644 --- a/src/routes/v2/tags.rs +++ b/src/routes/v2/tags.rs @@ -29,8 +29,6 @@ pub struct CategoryData { header: String, } -// TODO: searching / filtering? Could be used to implement a live -// searching category list #[get("category")] pub async fn category_list(pool: web::Data) -> Result { let results = Category::list(&**pool) diff --git a/src/routes/v2/teams.rs b/src/routes/v2/teams.rs index 64802a59c..bc1f8cf6b 100644 --- a/src/routes/v2/teams.rs +++ b/src/routes/v2/teams.rs @@ -3,9 +3,10 @@ use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::TeamMember; use crate::models::ids::ProjectId; use crate::models::notifications::NotificationBody; +use crate::models::pats::Scopes; use crate::models::teams::{Permissions, TeamId}; use crate::models::users::UserId; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::routes::ApiError; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use rust_decimal::Decimal; @@ -32,15 +33,22 @@ pub async fn team_members_get_project( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let string = info.into_inner().0; let project_data = crate::database::models::Project::get(&string, &**pool, &redis).await?; if let Some(project) = project_data { - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); if !is_authorized(&project.inner, ¤t_user, &pool).await? { return Ok(HttpResponse::NotFound().body("")); @@ -93,7 +101,7 @@ pub async fn team_members_get( info: web::Path<(TeamId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let id = info.into_inner().0; let members_data = TeamMember::get_from_team_full(id.into(), &**pool, &redis).await?; @@ -104,9 +112,16 @@ pub async fn team_members_get( ) .await?; - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); let user_id = current_user.as_ref().map(|x| x.id.into()); let logged_in = current_user @@ -148,7 +163,7 @@ pub async fn teams_get( web::Query(ids): web::Query, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { use itertools::Itertools; @@ -165,9 +180,16 @@ pub async fn teams_get( ) .await?; - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); let teams_groups = teams_data.into_iter().group_by(|data| data.team_id.0); @@ -206,10 +228,18 @@ pub async fn join_team( info: web::Path<(TeamId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let team_id = info.into_inner().0.into(); - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; let member = TeamMember::get_from_user_id_pending(team_id, current_user.id.into(), &**pool).await?; @@ -275,13 +305,21 @@ pub async fn add_team_member( pool: web::Data, new_member: web::Json, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let team_id = info.into_inner().0.into(); let mut transaction = pool.begin().await?; - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool) .await? .ok_or_else(|| { @@ -390,13 +428,21 @@ pub async fn edit_team_member( pool: web::Data, edit_member: web::Json, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let ids = info.into_inner(); let id = ids.0.into(); let user_id = ids.1.into(); - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) .await? .ok_or_else(|| { @@ -481,11 +527,19 @@ pub async fn transfer_ownership( pool: web::Data, new_owner: web::Json, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let id = info.into_inner().0; - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; if !current_user.role.is_admin() { let member = TeamMember::get_from_user_id(id.into(), current_user.id.into(), &**pool) @@ -554,13 +608,21 @@ pub async fn remove_team_member( info: web::Path<(TeamId, UserId)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let ids = info.into_inner(); let id = ids.0.into(); let user_id = ids.1.into(); - let current_user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let current_user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_WRITE]), + ) + .await? + .1; let member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) .await? .ok_or_else(|| { diff --git a/src/routes/v2/threads.rs b/src/routes/v2/threads.rs index 3a64f3c0c..89b67ceef 100644 --- a/src/routes/v2/threads.rs +++ b/src/routes/v2/threads.rs @@ -4,10 +4,11 @@ use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::thread_item::ThreadMessageBuilder; use crate::models::ids::ThreadMessageId; use crate::models::notifications::NotificationBody; +use crate::models::pats::Scopes; use crate::models::projects::ProjectStatus; use crate::models::threads::{MessageBody, Thread, ThreadId, ThreadType}; use crate::models::users::User; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::routes::ApiError; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; use futures::TryStreamExt; @@ -38,28 +39,34 @@ pub async fn is_authorized_thread( let user_id: database::models::UserId = user.id.into(); Ok(match thread.type_ { ThreadType::Report => { - let report_exists = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM reports WHERE thread_id = $1 AND reporter = $2)", - thread.id as database::models::ids::ThreadId, - user_id as database::models::ids::UserId, - ) - .fetch_one(pool) - .await? - .exists; + if let Some(report_id) = thread.report_id { + let report_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM reports WHERE id = $1 AND reporter = $2)", + report_id as database::models::ids::ReportId, + user_id as database::models::ids::UserId, + ) + .fetch_one(pool) + .await? + .exists; - report_exists.unwrap_or(false) + report_exists.unwrap_or(false) + } else { false } } ThreadType::Project => { - let project_exists = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 WHERE thread_id = $1)", - thread.id as database::models::ids::ThreadId, - user_id as database::models::ids::UserId, - ) - .fetch_one(pool) - .await? - .exists; + if let Some(project_id) = thread.project_id { + let project_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 WHERE m.id = $1)", + project_id as database::models::ids::ProjectId, + user_id as database::models::ids::UserId, + ) + .fetch_one(pool) + .await? + .exists; - project_exists.unwrap_or(false) + project_exists.unwrap_or(false) + } else { + false + } } ThreadType::DirectMessage => thread.members.contains(&user_id), }) @@ -90,15 +97,15 @@ pub async fn filter_authorized_threads( let project_thread_ids = check_threads .iter() .filter(|x| x.type_ == ThreadType::Project) - .map(|x| x.id.0) + .flat_map(|x| x.project_id.map(|x| x.0)) .collect::>(); if !project_thread_ids.is_empty() { sqlx::query!( " - SELECT m.thread_id FROM mods m + SELECT m.id FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 - WHERE m.thread_id = ANY($1) + WHERE m.id = ANY($1) ", &*project_thread_ids, user_id as database::models::ids::UserId, @@ -107,7 +114,7 @@ pub async fn filter_authorized_threads( .try_for_each(|e| { if let Some(row) = e.right() { check_threads.retain(|x| { - let bool = Some(x.id.0) == row.thread_id; + let bool = x.project_id.map(|x| x.0) == Some(row.id); if bool { return_threads.push(x.clone()); @@ -125,14 +132,14 @@ pub async fn filter_authorized_threads( let report_thread_ids = check_threads .iter() .filter(|x| x.type_ == ThreadType::Report) - .map(|x| x.id.0) + .flat_map(|x| x.report_id.map(|x| x.0)) .collect::>(); if !report_thread_ids.is_empty() { sqlx::query!( " - SELECT thread_id FROM reports - WHERE thread_id = ANY($1) AND reporter = $2 + SELECT id FROM reports + WHERE id = ANY($1) AND reporter = $2 ", &*report_thread_ids, user_id as database::models::ids::UserId, @@ -141,7 +148,7 @@ pub async fn filter_authorized_threads( .try_for_each(|e| { if let Some(row) = e.right() { check_threads.retain(|x| { - let bool = Some(x.id.0) == row.thread_id; + let bool = x.report_id.map(|x| x.0) == Some(row.id); if bool { return_threads.push(x.clone()); @@ -212,13 +219,21 @@ pub async fn thread_get( info: web::Path<(ThreadId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let string = info.into_inner().0.into(); let thread_data = database::models::Thread::get(string, &**pool).await?; - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::THREAD_READ]), + ) + .await? + .1; if let Some(mut data) = thread_data { if is_authorized_thread(&data, &user, &pool).await? { @@ -255,9 +270,17 @@ pub async fn threads_get( web::Query(ids): web::Query, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::THREAD_READ]), + ) + .await? + .1; let thread_ids: Vec = serde_json::from_str::>(&ids.ids)? @@ -284,9 +307,17 @@ pub async fn thread_send_message( pool: web::Data, new_message: web::Json, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::THREAD_WRITE]), + ) + .await? + .1; let string: database::models::ThreadId = info.into_inner().0.into(); @@ -347,50 +378,45 @@ pub async fn thread_send_message( .insert(&mut transaction) .await?; - let mod_notif = if thread.type_ == ThreadType::Project { - let record = sqlx::query!( - "SELECT m.id, m.status, m.team_id FROM mods m WHERE thread_id = $1", - thread.id as database::models::ids::ThreadId, + let mod_notif = if let Some(project_id) = thread.project_id { + let project = database::models::Project::get_id( + project_id, + &**pool, + &redis, ) - .fetch_one(&**pool) - .await?; - - let status = ProjectStatus::from_str(&record.status); - - if status != ProjectStatus::Processing && user.role.is_mod() { - let members = database::models::TeamMember::get_from_team_full( - database::models::TeamId(record.team_id), - &**pool, - &redis, - ) .await?; - NotificationBuilder { - body: NotificationBody::ModeratorMessage { - thread_id: thread.id.into(), - message_id: id.into(), - project_id: Some(database::models::ProjectId(record.id).into()), - report_id: None, - }, + if let Some(project) = project { + if project.inner.status != ProjectStatus::Processing && user.role.is_mod() { + let members = database::models::TeamMember::get_from_team_full( + project.inner.team_id, + &**pool, + &redis, + ) + .await?; + + NotificationBuilder { + body: NotificationBody::ModeratorMessage { + thread_id: thread.id.into(), + message_id: id.into(), + project_id: Some(project.inner.id.into()), + report_id: None, + }, + } + .insert_many( + members.into_iter().map(|x| x.user_id).collect(), + &mut transaction, + ) + .await?; } - .insert_many( - members.into_iter().map(|x| x.user_id).collect(), - &mut transaction, - ) - .await?; + + project.inner.status == ProjectStatus::Processing && !user.role.is_mod() + } else { + !user.role.is_mod() } - - status == ProjectStatus::Processing && !user.role.is_mod() - } else if thread.type_ == ThreadType::Report { - let record = sqlx::query!( - "SELECT r.id FROM reports r WHERE thread_id = $1", - thread.id as database::models::ids::ThreadId, - ) - .fetch_one(&**pool) - .await?; - + } else if let Some(report_id) = thread.report_id { let report = database::models::report_item::Report::get( - database::models::ReportId(record.id), + report_id, &**pool, ) .await?; @@ -446,7 +472,7 @@ pub async fn moderation_inbox( req: HttpRequest, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let user = check_is_moderator_from_headers(&req, &**pool, &redis, &session_queue).await?; @@ -474,7 +500,7 @@ pub async fn thread_read( info: web::Path<(ThreadId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { check_is_moderator_from_headers(&req, &**pool, &redis, &session_queue).await?; @@ -503,9 +529,17 @@ pub async fn message_delete( info: web::Path<(ThreadMessageId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::THREAD_WRITE]), + ) + .await? + .1; let result = database::models::ThreadMessage::get(info.into_inner().0.into(), &**pool).await?; diff --git a/src/routes/v2/users.rs b/src/routes/v2/users.rs index b3475131e..f9630c42e 100644 --- a/src/routes/v2/users.rs +++ b/src/routes/v2/users.rs @@ -1,11 +1,12 @@ -use crate::auth::get_user_from_headers; +use crate::auth::{get_user_from_headers, AuthenticationError}; use crate::database::models::User; use crate::file_hosting::FileHost; use crate::models::notifications::Notification; +use crate::models::pats::Scopes; use crate::models::projects::Project; use crate::models::users::{Badges, RecipientType, RecipientWallet, Role, UserId}; use crate::queue::payouts::{PayoutAmount, PayoutItem, PayoutsQueue}; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::routes::read_from_payload; use crate::util::validate::validation_errors_to_string; @@ -27,7 +28,6 @@ use validator::Validate; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(user_auth_get); - cfg.service(user_data_get); cfg.service(users_get); cfg.service( @@ -49,51 +49,36 @@ pub async fn user_auth_get( req: HttpRequest, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - Ok( - HttpResponse::Ok() - .json(get_user_from_headers(&req, &**pool, &redis, &session_queue).await?), + let (scopes, mut user) = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::USER_READ]), ) -} + .await?; -#[derive(Serialize)] -pub struct UserData { - pub notifs_count: u64, - pub followed_projects: Vec, -} - -#[get("user_data")] -pub async fn user_data_get( - req: HttpRequest, - pool: web::Data, - redis: web::Data, - session_queue: web::Data, -) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; - - let data = sqlx::query!( - " - SELECT COUNT(DISTINCT n.id) notifs_count, ARRAY_AGG(mf.mod_id) followed_projects FROM notifications n - LEFT OUTER JOIN mod_follows mf ON mf.follower_id = $1 - WHERE user_id = $1 AND read = FALSE - ", - user.id.0 as i64 - ).fetch_optional(&**pool).await?; - - if let Some(data) = data { - Ok(HttpResponse::Ok().json(UserData { - notifs_count: data.notifs_count.map(|x| x as u64).unwrap_or(0), - followed_projects: data - .followed_projects - .unwrap_or_default() - .into_iter() - .map(|x| crate::models::ids::ProjectId(x as u64)) - .collect(), - })) - } else { - Ok(HttpResponse::NoContent().body("")) + if !scopes.contains(Scopes::USER_READ_EMAIL) { + user.email = None; } + + if !scopes.contains(Scopes::PAYOUTS_READ) { + user.payout_data = None; + } + + Ok(HttpResponse::Ok().json( + get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::USER_READ]), + ) + .await? + .1, + )) } #[derive(Serialize, Deserialize)] @@ -138,11 +123,18 @@ pub async fn projects_list( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ]), + ) + .await + .map(|x| x.1) + .ok(); let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; @@ -218,9 +210,16 @@ pub async fn user_edit( new_user: web::Json, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let (scopes, user) = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::USER_WRITE]), + ) + .await?; new_user .validate() @@ -343,6 +342,12 @@ pub async fn user_edit( )); } + if !scopes.contains(Scopes::PAYOUTS_WRITE) { + return Err(ApiError::Authentication( + AuthenticationError::InvalidCredentials, + )); + } + if !match payout_data.payout_wallet_type { RecipientType::Email => { validator::validate_email(&payout_data.payout_address) @@ -401,6 +406,12 @@ pub async fn user_edit( } if let Some((old_password, new_password)) = &new_user.password { + if !scopes.contains(Scopes::USER_AUTH_WRITE) { + return Err(ApiError::Authentication( + AuthenticationError::InvalidCredentials, + )); + } + if let Some(pass) = actual_user.password { let old_password = old_password.as_ref().ok_or_else(|| { ApiError::CustomAuthentication( @@ -500,11 +511,19 @@ pub async fn user_icon_edit( redis: web::Data, file_host: web::Data>, mut payload: web::Payload, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) { let cdn_url = dotenvy::var("CDN_URL")?; - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::USER_WRITE]), + ) + .await? + .1; let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(actual_user) = id_option { @@ -579,9 +598,17 @@ pub async fn user_delete( pool: web::Data, removal_type: web::Query, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::USER_DELETE]), + ) + .await? + .1; let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { @@ -619,9 +646,17 @@ pub async fn user_follows( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::USER_READ]), + ) + .await? + .1; let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { @@ -667,9 +702,17 @@ pub async fn user_notifications( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::NOTIFICATION_READ]), + ) + .await? + .1; let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { @@ -707,9 +750,17 @@ pub async fn user_payouts( info: web::Path<(String,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PAYOUTS_READ]), + ) + .await? + .1; let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { @@ -784,11 +835,19 @@ pub async fn user_payouts_request( data: web::Json, payouts_queue: web::Data>, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let mut payouts_queue = payouts_queue.lock().await; - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PAYOUTS_WRITE]), + ) + .await? + .1; let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?; if let Some(id) = id_option.map(|x| x.id) { diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index 05ed58337..caf972dca 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -8,12 +8,13 @@ use crate::database::models::version_item::{ use crate::file_hosting::FileHost; use crate::models::notifications::NotificationBody; use crate::models::pack::PackFileHash; +use crate::models::pats::Scopes; use crate::models::projects::{ Dependency, DependencyType, FileType, GameVersion, Loader, ProjectId, Version, VersionFile, VersionId, VersionStatus, VersionType, }; use crate::models::teams::Permissions; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::util::routes::read_from_field; use crate::util::validate::validation_errors_to_string; use crate::validate::{validate_file, ValidationResult}; @@ -85,7 +86,7 @@ pub async fn version_create( client: Data, redis: Data, file_host: Data>, - session_queue: Data, + session_queue: Data, ) -> Result { let mut transaction = client.begin().await?; let mut uploaded_files = Vec::new(); @@ -127,7 +128,7 @@ async fn version_create_inner( file_host: &dyn FileHost, uploaded_files: &mut Vec, pool: &PgPool, - session_queue: &SessionQueue, + session_queue: &AuthQueue, ) -> Result { let cdn_url = dotenvy::var("CDN_URL")?; @@ -137,7 +138,15 @@ async fn version_create_inner( let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?; let all_loaders = models::categories::Loader::list(&mut *transaction).await?; - let user = get_user_from_headers(&req, pool, redis, session_queue).await?; + let user = get_user_from_headers( + &req, + pool, + redis, + session_queue, + Some(&[Scopes::VERSION_CREATE]), + ) + .await? + .1; let mut error = None; while let Some(item) = payload.next().await { @@ -441,7 +450,7 @@ pub async fn upload_file_to_version( client: Data, redis: Data, file_host: Data>, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let mut transaction = client.begin().await?; let mut uploaded_files = Vec::new(); @@ -487,14 +496,22 @@ async fn upload_file_to_version_inner( file_host: &dyn FileHost, uploaded_files: &mut Vec, version_id: models::VersionId, - session_queue: &SessionQueue, + session_queue: &AuthQueue, ) -> Result { let cdn_url = dotenvy::var("CDN_URL")?; let mut initial_file_data: Option = None; let mut file_builders: Vec = Vec::new(); - let user = get_user_from_headers(&req, &**client, &redis, session_queue).await?; + let user = get_user_from_headers( + &req, + &**client, + &redis, + session_queue, + Some(&[Scopes::VERSION_WRITE]), + ) + .await? + .1; let result = models::Version::get(version_id, &**client, &redis).await?; diff --git a/src/routes/v2/version_file.rs b/src/routes/v2/version_file.rs index bf36ec2f7..c0953679f 100644 --- a/src/routes/v2/version_file.rs +++ b/src/routes/v2/version_file.rs @@ -4,9 +4,10 @@ use crate::auth::{ is_authorized_version, }; use crate::models::ids::VersionId; +use crate::models::pats::Scopes; use crate::models::projects::VersionType; use crate::models::teams::Permissions; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::{database, models}; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; use itertools::Itertools; @@ -49,11 +50,18 @@ pub async fn get_version_from_hash( pool: web::Data, redis: web::Data, hash_query: web::Query, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); let hash = info.into_inner().0.to_lowercase(); let file = database::models::Version::get_file_from_hash( @@ -95,11 +103,18 @@ pub async fn download_version( pool: web::Data, redis: web::Data, hash_query: web::Query, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); let hash = info.into_inner().0.to_lowercase(); let file = database::models::Version::get_file_from_hash( @@ -138,9 +153,17 @@ pub async fn delete_file( pool: web::Data, redis: web::Data, hash_query: web::Query, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_WRITE]), + ) + .await? + .1; let hash = info.into_inner().0.to_lowercase(); @@ -234,11 +257,18 @@ pub async fn get_update_from_hash( redis: web::Data, hash_query: web::Query, update_data: web::Json, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); let hash = info.into_inner().0.to_lowercase(); if let Some(file) = database::models::Version::get_file_from_hash( @@ -304,11 +334,18 @@ pub async fn get_versions_from_hashes( pool: web::Data, redis: web::Data, file_data: web::Json, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); let files = database::models::Version::get_files_from_hash( file_data.algorithm.clone(), @@ -345,11 +382,18 @@ pub async fn get_projects_from_hashes( pool: web::Data, redis: web::Data, file_data: web::Json, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); let files = database::models::Version::get_files_from_hash( file_data.algorithm.clone(), @@ -396,11 +440,18 @@ pub async fn update_files( pool: web::Data, redis: web::Data, update_data: web::Json, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); let files = database::models::Version::get_files_from_hash( update_data.algorithm.clone(), diff --git a/src/routes/v2/versions.rs b/src/routes/v2/versions.rs index fed35ec61..8367c7629 100644 --- a/src/routes/v2/versions.rs +++ b/src/routes/v2/versions.rs @@ -4,9 +4,10 @@ use crate::auth::{ }; use crate::database; use crate::models; +use crate::models::pats::Scopes; use crate::models::projects::{Dependency, FileType, VersionStatus, VersionType}; use crate::models::teams::Permissions; -use crate::queue::session::SessionQueue; +use crate::queue::session::AuthQueue; use crate::util::validate::validation_errors_to_string; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use chrono::{DateTime, Utc}; @@ -45,15 +46,22 @@ pub async fn version_list( web::Query(filters): web::Query, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let string = info.into_inner().0; let result = database::models::Project::get(&string, &**pool, &redis).await?; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ, Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); if let Some(project) = result { if !is_authorized(&project.inner, &user_option, &pool).await? { @@ -154,15 +162,22 @@ pub async fn version_project_get( info: web::Path<(String, String)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let id = info.into_inner(); let version_data = database::models::Version::get_full_from_id_slug(&id.0, &id.1, &**pool, &redis).await?; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::PROJECT_READ, Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); if let Some(data) = version_data { if is_authorized_version(&data.inner, &user_option, &pool).await? { @@ -184,7 +199,7 @@ pub async fn versions_get( web::Query(ids): web::Query, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let version_ids = serde_json::from_str::>(&ids.ids)? .into_iter() @@ -192,9 +207,16 @@ pub async fn versions_get( .collect::>(); let versions_data = database::models::Version::get_many(&version_ids, &**pool, &redis).await?; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); let versions = filter_authorized_versions(versions_data, &user_option, &pool).await?; @@ -207,14 +229,21 @@ pub async fn version_get( info: web::Path<(models::ids::VersionId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { let id = info.into_inner().0; let version_data = database::models::Version::get(id.into(), &**pool, &redis).await?; - let user_option = get_user_from_headers(&req, &**pool, &redis, &session_queue) - .await - .ok(); + let user_option = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_READ]), + ) + .await + .map(|x| x.1) + .ok(); if let Some(data) = version_data { if is_authorized_version(&data.inner, &user_option, &pool).await? { @@ -268,9 +297,17 @@ pub async fn version_edit( pool: web::Data, redis: web::Data, new_version: web::Json, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_WRITE]), + ) + .await? + .1; new_version .validate() @@ -645,9 +682,17 @@ pub async fn version_schedule( pool: web::Data, redis: web::Data, scheduling_data: web::Json, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_WRITE]), + ) + .await? + .1; if scheduling_data.time < Utc::now() { return Err(ApiError::InvalidInput( @@ -711,9 +756,17 @@ pub async fn version_delete( info: web::Path<(models::ids::VersionId,)>, pool: web::Data, redis: web::Data, - session_queue: web::Data, + session_queue: web::Data, ) -> Result { - let user = get_user_from_headers(&req, &**pool, &redis, &session_queue).await?; + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::VERSION_DELETE]), + ) + .await? + .1; let id = info.into_inner().0; let version = database::models::Version::get(id.into(), &**pool, &redis)