diff --git a/.dockerignore b/.dockerignore index d8db2f7..9dbf30b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,5 @@ target Dockerfile .dockerignore .git -.gitignore \ No newline at end of file +.gitignore +.env \ No newline at end of file diff --git a/.env.example b/.env.example index 965b582..db402dd 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,9 @@ APP_DEBUG=0 # Used for URL generation in downloads APP_URL=http://localhost DATABASE_URL=postgres://user:password@localhost/schema -PORT=8080 \ No newline at end of file +PORT=8080 + +# GitHub + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= \ No newline at end of file diff --git a/.sqlx/query-073fa455f6071709fcf51f8c8e4a5bced00a3cacd10dc4637e7df2acc2c7ddb4.json b/.sqlx/query-073fa455f6071709fcf51f8c8e4a5bced00a3cacd10dc4637e7df2acc2c7ddb4.json new file mode 100644 index 0000000..1995183 --- /dev/null +++ b/.sqlx/query-073fa455f6071709fcf51f8c8e4a5bced00a3cacd10dc4637e7df2acc2c7ddb4.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, username, display_name, verified, github_user_id\n FROM developers WHERE github_user_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "display_name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "verified", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "github_user_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "073fa455f6071709fcf51f8c8e4a5bced00a3cacd10dc4637e7df2acc2c7ddb4" +} diff --git a/.sqlx/query-12d29fa3087ef95a089e5bc806c98776e26770bd312ed0a2202f2aab31eb2daa.json b/.sqlx/query-12d29fa3087ef95a089e5bc806c98776e26770bd312ed0a2202f2aab31eb2daa.json new file mode 100644 index 0000000..16f419b --- /dev/null +++ b/.sqlx/query-12d29fa3087ef95a089e5bc806c98776e26770bd312ed0a2202f2aab31eb2daa.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE github_login_attempts SET last_poll = $1 WHERE uid = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "12d29fa3087ef95a089e5bc806c98776e26770bd312ed0a2202f2aab31eb2daa" +} diff --git a/.sqlx/query-2acbb46366bb9db6aaff52eca74003580b205ac33accb324a929636bc8c782e4.json b/.sqlx/query-2d32049425b12e11afd24a7cbdc7f4115fac9c3ba899accceada903cf7c5f4f2.json similarity index 63% rename from .sqlx/query-2acbb46366bb9db6aaff52eca74003580b205ac33accb324a929636bc8c782e4.json rename to .sqlx/query-2d32049425b12e11afd24a7cbdc7f4115fac9c3ba899accceada903cf7c5f4f2.json index eb5a106..d3e1692 100644 --- a/.sqlx/query-2acbb46366bb9db6aaff52eca74003580b205ac33accb324a929636bc8c782e4.json +++ b/.sqlx/query-2d32049425b12e11afd24a7cbdc7f4115fac9c3ba899accceada903cf7c5f4f2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT latest_version, validated, about, changelog FROM mods WHERE id = $1", + "query": "SELECT DISTINCT\n m.latest_version, m.about, m.changelog FROM mods m\n INNER JOIN mod_versions mv ON mv.mod_id = m.id\n WHERE m.id = $1 AND mv.validated = true", "describe": { "columns": [ { @@ -10,16 +10,11 @@ }, { "ordinal": 1, - "name": "validated", - "type_info": "Bool" - }, - { - "ordinal": 2, "name": "about", "type_info": "Text" }, { - "ordinal": 3, + "ordinal": 2, "name": "changelog", "type_info": "Text" } @@ -30,11 +25,10 @@ ] }, "nullable": [ - false, false, true, true ] }, - "hash": "2acbb46366bb9db6aaff52eca74003580b205ac33accb324a929636bc8c782e4" + "hash": "2d32049425b12e11afd24a7cbdc7f4115fac9c3ba899accceada903cf7c5f4f2" } diff --git a/.sqlx/query-30858d08c9414e9671a0c0adcb1cc21ece0f8a811f08b86dd911d65dd64e10df.json b/.sqlx/query-30858d08c9414e9671a0c0adcb1cc21ece0f8a811f08b86dd911d65dd64e10df.json new file mode 100644 index 0000000..045c8a6 --- /dev/null +++ b/.sqlx/query-30858d08c9414e9671a0c0adcb1cc21ece0f8a811f08b86dd911d65dd64e10df.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO auth_tokens (developer_id) VALUES ($1) returning token", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "30858d08c9414e9671a0c0adcb1cc21ece0f8a811f08b86dd911d65dd64e10df" +} diff --git a/.sqlx/query-38b0c32de5c085b0cd407f67b9d14747b936ce248d49a7dfbe549b25bfaa2058.json b/.sqlx/query-38b0c32de5c085b0cd407f67b9d14747b936ce248d49a7dfbe549b25bfaa2058.json new file mode 100644 index 0000000..0e2db23 --- /dev/null +++ b/.sqlx/query-38b0c32de5c085b0cd407f67b9d14747b936ce248d49a7dfbe549b25bfaa2058.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT uid as uuid, ip, device_code, interval, expires_in, created_at, last_poll\n FROM github_login_attempts\n WHERE ip = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "uuid", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "ip", + "type_info": "Inet" + }, + { + "ordinal": 2, + "name": "device_code", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "interval", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "expires_in", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "last_poll", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Inet" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "38b0c32de5c085b0cd407f67b9d14747b936ce248d49a7dfbe549b25bfaa2058" +} diff --git a/.sqlx/query-4cf4f4e935b7f94c003deff3d4f5c4864ba142fe2aec0b6c67798da32d73a8e9.json b/.sqlx/query-4cf4f4e935b7f94c003deff3d4f5c4864ba142fe2aec0b6c67798da32d73a8e9.json deleted file mode 100644 index 55fa782..0000000 --- a/.sqlx/query-4cf4f4e935b7f94c003deff3d4f5c4864ba142fe2aec0b6c67798da32d73a8e9.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) \n FROM mods m\n INNER JOIN mod_versions mv ON m.id = mv.mod_id\n INNER JOIN mod_gd_versions mgv ON mgv.mod_id = mv.id\n WHERE m.validated = true AND mv.name LIKE $1 AND mgv.gd = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - { - "Custom": { - "name": "gd_version", - "kind": { - "Enum": [ - "*", - "2.113", - "2.200", - "2.204", - "2.205" - ] - } - } - } - ] - }, - "nullable": [ - null - ] - }, - "hash": "4cf4f4e935b7f94c003deff3d4f5c4864ba142fe2aec0b6c67798da32d73a8e9" -} diff --git a/.sqlx/query-5156837d9a48e476ce40f5e3b5554ed2fa633e9767072e20da98554c808ec169.json b/.sqlx/query-503bcbb637879d3f142d7826a03e395dde64bb4f5799386eb8219ad3bdfdbf93.json similarity index 72% rename from .sqlx/query-5156837d9a48e476ce40f5e3b5554ed2fa633e9767072e20da98554c808ec169.json rename to .sqlx/query-503bcbb637879d3f142d7826a03e395dde64bb4f5799386eb8219ad3bdfdbf93.json index 2c635a7..d1eda07 100644 --- a/.sqlx/query-5156837d9a48e476ce40f5e3b5554ed2fa633e9767072e20da98554c808ec169.json +++ b/.sqlx/query-503bcbb637879d3f142d7826a03e395dde64bb4f5799386eb8219ad3bdfdbf93.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT mv.*, m.changelog, m.about FROM mod_versions mv\n INNER JOIN mods m ON m.id = mv.mod_id\n WHERE mv.mod_id = $1 AND mv.version = $2", + "query": "SELECT\n mv.id, mv.name, mv.description, mv.version, mv.download_link,\n mv.hash, mv.geode, mv.early_load, mv.api, mv.mod_id FROM mod_versions mv\n INNER JOIN mods m ON m.id = mv.mod_id\n WHERE mv.mod_id = $1 AND mv.version = $2 AND mv.validated = true", "describe": { "columns": [ { @@ -52,16 +52,6 @@ "ordinal": 9, "name": "mod_id", "type_info": "Text" - }, - { - "ordinal": 10, - "name": "changelog", - "type_info": "Text" - }, - { - "ordinal": 11, - "name": "about", - "type_info": "Text" } ], "parameters": { @@ -80,10 +70,8 @@ false, false, false, - false, - true, - true + false ] }, - "hash": "5156837d9a48e476ce40f5e3b5554ed2fa633e9767072e20da98554c808ec169" + "hash": "503bcbb637879d3f142d7826a03e395dde64bb4f5799386eb8219ad3bdfdbf93" } diff --git a/.sqlx/query-50e05697086db487830c93bc21dc8ae15e7079364b0b0b2b8ce86434a07f7de1.json b/.sqlx/query-50e05697086db487830c93bc21dc8ae15e7079364b0b0b2b8ce86434a07f7de1.json new file mode 100644 index 0000000..55d261b --- /dev/null +++ b/.sqlx/query-50e05697086db487830c93bc21dc8ae15e7079364b0b0b2b8ce86434a07f7de1.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO developers \n (username, display_name, github_user_id) VALUES\n ($1, $2, $3) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "50e05697086db487830c93bc21dc8ae15e7079364b0b0b2b8ce86434a07f7de1" +} diff --git a/.sqlx/query-5be8bf45e462225ff08f63ed0647a8c8b2123767132828b7f56981ed7083acf2.json b/.sqlx/query-5be8bf45e462225ff08f63ed0647a8c8b2123767132828b7f56981ed7083acf2.json new file mode 100644 index 0000000..443d82f --- /dev/null +++ b/.sqlx/query-5be8bf45e462225ff08f63ed0647a8c8b2123767132828b7f56981ed7083acf2.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, version FROM mod_versions WHERE mod_id = $1 and validated = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "version", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "5be8bf45e462225ff08f63ed0647a8c8b2123767132828b7f56981ed7083acf2" +} diff --git a/.sqlx/query-6da5d07746c3fbdc7a2ca5aac801d46cc9d17bf89a47a5e98c901dceffb4b3bd.json b/.sqlx/query-6da5d07746c3fbdc7a2ca5aac801d46cc9d17bf89a47a5e98c901dceffb4b3bd.json deleted file mode 100644 index 4ef75ba..0000000 --- a/.sqlx/query-6da5d07746c3fbdc7a2ca5aac801d46cc9d17bf89a47a5e98c901dceffb4b3bd.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT DISTINCT \n m.id, m.repository, m.latest_version, m.validated FROM mods m\n INNER JOIN mod_versions mv ON m.id = mv.mod_id \n INNER JOIN mod_gd_versions mgv ON mgv.mod_id = mv.id\n WHERE m.validated = true AND mv.name LIKE $1 AND mgv.gd = $2\n LIMIT $3 OFFSET $4", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "repository", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "latest_version", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "validated", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - { - "Custom": { - "name": "gd_version", - "kind": { - "Enum": [ - "*", - "2.113", - "2.200", - "2.204", - "2.205" - ] - } - } - }, - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - true, - false, - false - ] - }, - "hash": "6da5d07746c3fbdc7a2ca5aac801d46cc9d17bf89a47a5e98c901dceffb4b3bd" -} diff --git a/.sqlx/query-70db4ee34c2c0a34c323d9bab8fb427328e9fae10c8e8ff3a12a53578f636c8a.json b/.sqlx/query-70db4ee34c2c0a34c323d9bab8fb427328e9fae10c8e8ff3a12a53578f636c8a.json index 41238b4..d8be92f 100644 --- a/.sqlx/query-70db4ee34c2c0a34c323d9bab8fb427328e9fae10c8e8ff3a12a53578f636c8a.json +++ b/.sqlx/query-70db4ee34c2c0a34c323d9bab8fb427328e9fae10c8e8ff3a12a53578f636c8a.json @@ -39,7 +39,8 @@ "name": "gd_ver_platform", "kind": { "Enum": [ - "android", + "android32", + "android64", "ios", "mac", "win" diff --git a/.sqlx/query-01bd480b1054bbc29c7d142eedfb05e2b3237b16153d5d00cef5c2a7ba7285b4.json b/.sqlx/query-92c96e237bb0fc81b3b7fac2565c34365559989461186c6d2ef2aef11449c1d0.json similarity index 90% rename from .sqlx/query-01bd480b1054bbc29c7d142eedfb05e2b3237b16153d5d00cef5c2a7ba7285b4.json rename to .sqlx/query-92c96e237bb0fc81b3b7fac2565c34365559989461186c6d2ef2aef11449c1d0.json index 661b01c..5acb720 100644 --- a/.sqlx/query-01bd480b1054bbc29c7d142eedfb05e2b3237b16153d5d00cef5c2a7ba7285b4.json +++ b/.sqlx/query-92c96e237bb0fc81b3b7fac2565c34365559989461186c6d2ef2aef11449c1d0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT icp.compare as \"compare: ModVersionCompare\",\n icp.importance as \"importance: IncompatibilityImportance\",\n icp.incompatibility_id, mv.mod_id, mv.version FROM incompatibilities icp\n INNER JOIN mod_versions mv ON icp.mod_id = mv.id\n WHERE mv.id = $1", + "query": "SELECT icp.compare as \"compare: ModVersionCompare\",\n icp.importance as \"importance: IncompatibilityImportance\",\n icp.incompatibility_id, mv.mod_id, mv.version FROM incompatibilities icp\n INNER JOIN mod_versions mv ON icp.mod_id = mv.id\n WHERE mv.id = $1 AND mv.validated = true", "describe": { "columns": [ { @@ -65,5 +65,5 @@ false ] }, - "hash": "01bd480b1054bbc29c7d142eedfb05e2b3237b16153d5d00cef5c2a7ba7285b4" + "hash": "92c96e237bb0fc81b3b7fac2565c34365559989461186c6d2ef2aef11449c1d0" } diff --git a/.sqlx/query-b187c841dad18974779621a70d81e5490e7cc982d5cc5ca68ec080e33110d082.json b/.sqlx/query-b187c841dad18974779621a70d81e5490e7cc982d5cc5ca68ec080e33110d082.json deleted file mode 100644 index 0beb420..0000000 --- a/.sqlx/query-b187c841dad18974779621a70d81e5490e7cc982d5cc5ca68ec080e33110d082.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE mods \n SET latest_version = $1, changelog = $2, about = $3\n WHERE id = $4", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "b187c841dad18974779621a70d81e5490e7cc982d5cc5ca68ec080e33110d082" -} diff --git a/.sqlx/query-4b29cde85703a13451fe0c007330b533a8758ced46a7c350f46260c7a76283da.json b/.sqlx/query-b7e31d7c81fa04928e659cf5acfb71719da68329f5b1f568774359cae28e997e.json similarity index 73% rename from .sqlx/query-4b29cde85703a13451fe0c007330b533a8758ced46a7c350f46260c7a76283da.json rename to .sqlx/query-b7e31d7c81fa04928e659cf5acfb71719da68329f5b1f568774359cae28e997e.json index a965e63..a141ce3 100644 --- a/.sqlx/query-4b29cde85703a13451fe0c007330b533a8758ced46a7c350f46260c7a76283da.json +++ b/.sqlx/query-b7e31d7c81fa04928e659cf5acfb71719da68329f5b1f568774359cae28e997e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n m.id, m.repository, m.latest_version, m.validated,\n mv.id as version_id, mv.name, mv.description, mv.version, mv.download_link,\n mv.hash, mv.geode, mv.early_load, mv.api, mv.mod_id\n FROM mods m\n INNER JOIN mod_versions mv ON m.id = mv.mod_id\n WHERE m.id = $1", + "query": "SELECT\n m.id, m.repository, m.latest_version, mv.validated, m.about, m.changelog,\n mv.id as version_id, mv.name, mv.description, mv.version, mv.download_link,\n mv.hash, mv.geode, mv.early_load, mv.api, mv.mod_id\n FROM mods m\n INNER JOIN mod_versions mv ON m.id = mv.mod_id\n WHERE m.id = $1 AND mv.validated = true", "describe": { "columns": [ { @@ -25,51 +25,61 @@ }, { "ordinal": 4, + "name": "about", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "changelog", + "type_info": "Text" + }, + { + "ordinal": 6, "name": "version_id", "type_info": "Int4" }, { - "ordinal": 5, + "ordinal": 7, "name": "name", "type_info": "Text" }, { - "ordinal": 6, + "ordinal": 8, "name": "description", "type_info": "Text" }, { - "ordinal": 7, + "ordinal": 9, "name": "version", "type_info": "Text" }, { - "ordinal": 8, + "ordinal": 10, "name": "download_link", "type_info": "Text" }, { - "ordinal": 9, + "ordinal": 11, "name": "hash", "type_info": "Text" }, { - "ordinal": 10, + "ordinal": 12, "name": "geode", "type_info": "Text" }, { - "ordinal": 11, + "ordinal": 13, "name": "early_load", "type_info": "Bool" }, { - "ordinal": 12, + "ordinal": 14, "name": "api", "type_info": "Bool" }, { - "ordinal": 13, + "ordinal": 15, "name": "mod_id", "type_info": "Text" } @@ -84,6 +94,8 @@ true, false, false, + true, + true, false, false, true, @@ -96,5 +108,5 @@ false ] }, - "hash": "4b29cde85703a13451fe0c007330b533a8758ced46a7c350f46260c7a76283da" + "hash": "b7e31d7c81fa04928e659cf5acfb71719da68329f5b1f568774359cae28e997e" } diff --git a/.sqlx/query-bf999ca230419894e5a2c7281596fe82e1a2ebae23511b4cab1ae841309b0a11.json b/.sqlx/query-bf999ca230419894e5a2c7281596fe82e1a2ebae23511b4cab1ae841309b0a11.json new file mode 100644 index 0000000..20d2df9 --- /dev/null +++ b/.sqlx/query-bf999ca230419894e5a2c7281596fe82e1a2ebae23511b4cab1ae841309b0a11.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT uid as uuid, ip, device_code, interval, expires_in, created_at, last_poll\n FROM github_login_attempts\n WHERE uid = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "uuid", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "ip", + "type_info": "Inet" + }, + { + "ordinal": 2, + "name": "device_code", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "interval", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "expires_in", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "last_poll", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "bf999ca230419894e5a2c7281596fe82e1a2ebae23511b4cab1ae841309b0a11" +} diff --git a/.sqlx/query-c61eb668062a7f6b5767d936eec4e939d5b2dfd294992a7472f715d7a3a45649.json b/.sqlx/query-c61eb668062a7f6b5767d936eec4e939d5b2dfd294992a7472f715d7a3a45649.json new file mode 100644 index 0000000..e56a14c --- /dev/null +++ b/.sqlx/query-c61eb668062a7f6b5767d936eec4e939d5b2dfd294992a7472f715d7a3a45649.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM github_login_attempts WHERE uid = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "c61eb668062a7f6b5767d936eec4e939d5b2dfd294992a7472f715d7a3a45649" +} diff --git a/.sqlx/query-d69a5379c794703a8ba7831e3ac09880c9cc8f7b8f3ed7115beef051951869ca.json b/.sqlx/query-d69a5379c794703a8ba7831e3ac09880c9cc8f7b8f3ed7115beef051951869ca.json new file mode 100644 index 0000000..8386dcb --- /dev/null +++ b/.sqlx/query-d69a5379c794703a8ba7831e3ac09880c9cc8f7b8f3ed7115beef051951869ca.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO github_login_attempts\n (ip, device_code, interval, expires_in) VALUES\n ($1, $2, $3, $4) RETURNING uid\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "uid", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Inet", + "Text", + "Int4", + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d69a5379c794703a8ba7831e3ac09880c9cc8f7b8f3ed7115beef051951869ca" +} diff --git a/.sqlx/query-b3c97f4cdc38242159e6bda2fa6763461750ba76828484766c05b2642f34bbd9.json b/.sqlx/query-fbe1f59fc9a069245be7d47852f90e95efbe467d8bb67ee912774a7fc8256058.json similarity index 74% rename from .sqlx/query-b3c97f4cdc38242159e6bda2fa6763461750ba76828484766c05b2642f34bbd9.json rename to .sqlx/query-fbe1f59fc9a069245be7d47852f90e95efbe467d8bb67ee912774a7fc8256058.json index 9e82c78..b0ba5cd 100644 --- a/.sqlx/query-b3c97f4cdc38242159e6bda2fa6763461750ba76828484766c05b2642f34bbd9.json +++ b/.sqlx/query-fbe1f59fc9a069245be7d47852f90e95efbe467d8bb67ee912774a7fc8256058.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT download_link FROM mod_versions WHERE mod_id = $1 AND version = $2", + "query": "SELECT download_link FROM mod_versions WHERE mod_id = $1 AND version = $2 AND validated = true", "describe": { "columns": [ { @@ -19,5 +19,5 @@ false ] }, - "hash": "b3c97f4cdc38242159e6bda2fa6763461750ba76828484766c05b2642f34bbd9" + "hash": "fbe1f59fc9a069245be7d47852f90e95efbe467d8bb67ee912774a7fc8256058" } diff --git a/Cargo.lock b/Cargo.lock index d76b87c..513fc57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,6 +250,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.79" @@ -428,6 +443,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.48.5", +] + [[package]] name = "cipher" version = "0.4.4" @@ -1030,6 +1057,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1065,6 +1115,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "is-terminal" version = "0.4.10" @@ -1912,6 +1971,7 @@ dependencies = [ "atoi", "byteorder", "bytes", + "chrono", "crc", "crossbeam-queue", "dotenvy", @@ -1925,6 +1985,7 @@ dependencies = [ "hashlink", "hex", "indexmap", + "ipnetwork", "log", "memchr", "once_cell", @@ -1940,6 +2001,7 @@ dependencies = [ "tokio-stream", "tracing", "url", + "uuid", ] [[package]] @@ -1993,6 +2055,7 @@ dependencies = [ "bitflags 2.4.2", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -2021,6 +2084,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -2034,6 +2098,7 @@ dependencies = [ "base64", "bitflags 2.4.2", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -2045,6 +2110,7 @@ dependencies = [ "hkdf", "hmac", "home", + "ipnetwork", "itoa", "log", "md-5", @@ -2060,6 +2126,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -2070,6 +2137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -2084,6 +2152,7 @@ dependencies = [ "tracing", "url", "urlencoding", + "uuid", ] [[package]] @@ -2559,6 +2628,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 1e69792..de3976c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,9 @@ log = "0.4.20" futures = "0.3.30" serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0" -sqlx = { version = "0.7.3", features = ["postgres", "runtime-tokio"] } +sqlx = { version = "0.7.3", features = ["postgres", "runtime-tokio", "chrono", "uuid", "ipnetwork"] } tokio = { version = "1.35.1", features = ["rt", "macros", "rt-multi-thread"] } -reqwest = "0.11.23" +reqwest = { version = "0.11.23", features = ["json"]} uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics"]} zip = "0.6" sha256 = "1.5.0" diff --git a/migrations/20240102213218_first_migration.down.sql b/migrations/20240102213218_first_migration.down.sql index ab55e89..074e356 100644 --- a/migrations/20240102213218_first_migration.down.sql +++ b/migrations/20240102213218_first_migration.down.sql @@ -9,7 +9,9 @@ DROP TABLE IF EXISTS mod_versions; DROP TABLE IF EXISTS mods_developers; DROP TABLE IF EXISTS dependencies; DROP TABLE IF EXISTS mods; +DROP TABLE IF EXISTS auth_tokens; DROP TABLE IF EXISTS developers; +DROP TABLE IF EXISTS github_login_attempts; DROP INDEX IF EXISTS idx_version_id; diff --git a/migrations/20240102213218_first_migration.up.sql b/migrations/20240102213218_first_migration.up.sql index ac842ba..35be7d9 100644 --- a/migrations/20240102213218_first_migration.up.sql +++ b/migrations/20240102213218_first_migration.up.sql @@ -10,8 +10,7 @@ CREATE TABLE mods ( repository TEXT, changelog TEXT, about TEXT, - latest_version TEXT NOT NULL, - validated BOOLEAN NOT NULL + latest_version TEXT NOT NULL ); CREATE TABLE mod_versions ( @@ -24,6 +23,7 @@ CREATE TABLE mod_versions ( geode TEXT NOT NULL, early_load BOOLEAN NOT NULL DEFAULT false, api BOOLEAN NOT NULL DEFAULT false, + validated BOOLEAN NOT NULL, mod_id TEXT NOT NULL, FOREIGN KEY (mod_id) REFERENCES mods(id) ); @@ -76,7 +76,8 @@ CREATE TABLE developers ( id SERIAL PRIMARY KEY NOT NULL, username TEXT NOT NULL, display_name TEXT NOT NULL, - verified BOOLEAN NOT NULL, + verified BOOLEAN DEFAULT false NOT NULL, + admin BOOLEAN DEFAULT false NOT NULL, github_user_id BIGINT NOT NULL ); @@ -87,3 +88,21 @@ CREATE TABLE mods_developers ( FOREIGN KEY (mod_id) REFERENCES mods(id), FOREIGN KEY (developer_id) REFERENCES developers(id) ); + +CREATE TABLE auth_tokens ( + token UUID DEFAULT gen_random_uuid() NOT NULL, + developer_id INTEGER NOT NULL, + PRIMARY KEY(token), + FOREIGN KEY(developer_id) REFERENCES developers(id) +); + +CREATE TABLE github_login_attempts ( + uid UUID DEFAULT gen_random_uuid() NOT NULL, + ip inet NOT NULL, + device_code TEXT NOT NULL, + interval INTEGER NOT NULL, + expires_in INTEGER NOT NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + last_poll TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY (uid, ip) +); \ No newline at end of file diff --git a/openapi.yml b/openapi.yml index 4f7176a..7001d65 100644 --- a/openapi.yml +++ b/openapi.yml @@ -28,6 +28,12 @@ paths: in: query description: Geometry Dash version required: true + schema: + $ref: "#/components/schemas/GDVersionString" + - name: platforms + in: query + description: Platforms that mods have to support, comma separated [win,android32,android64,mac,ios] + example: 'win,android32,android64' schema: type: string - $ref: '#/components/parameters/GeodeVersionQuery' @@ -42,7 +48,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Mod' + $ref: "#/components/schemas/Mod" post: tags: - mods @@ -58,8 +64,8 @@ paths: type: string description: The download URL for the .geode file. You can use a Github Release for this. examples: - - https://github.com/geode-sdk/NodeIDs/releases/download/v1.0.0/geode.node-ids.geode - - https://github.com/geode-sdk/DevTools/releases/download/v1.3.0/geode.devtools.geode + - "https://github.com/geode-sdk/NodeIDs/releases/download/v1.0.0/geode.node-ids.geode" + - "https://github.com/geode-sdk/DevTools/releases/download/v1.3.0/geode.devtools.geode" responses: '204': @@ -190,6 +196,34 @@ components: - 1.3.0 - v1.0.0-beta + GDVersionString: + type: string + examples: + - 2.200 + - 2.204 + - 2.205 + + GDVersionObject: + type: object + properties: + win: + anyOf: + - type: 'null' + - $ref: "#/components/schemas/GDVersionString" + mac: + oneOf: + - type: 'null' + - $ref: "#/components/schemas/GDVersionString" + ios: + oneOf: + - type: 'null' + - $ref: "#/components/schemas/GDVersionString" + android: + oneOf: + - type: 'null' + - $ref: "#/components/schemas/GDVersionString" + + UserSimple: type: object properties: @@ -225,9 +259,11 @@ components: id: $ref: '#/components/schemas/ModID' repository: - type: string + oneOf: + - type: 'null' + - type: string examples: - - 'https://github.com/geode-sdk/geode' + - "https://github.com/geode-sdk/geode" latest_version: $ref: '#/components/schemas/ModVersionString' validated: @@ -251,17 +287,29 @@ components: - Devtools description: type: string - geode_version: + early_load: + type: boolean + api: + type: boolean + geode: $ref: '#/components/schemas/ModVersionString' version: $ref: '#/components/schemas/ModVersionString' download_link: type: string examples: - - 'https://github.com/geode-sdk/DevTools/releases/download/v1.3.0/geode.devtools.geode' + - "https://api.geode-sdk.com/v1/mods/geode.nodeids/versions/1.0.0/download" + - "https://api.geode-sdk.com/v1/mods/geode.devtools/versions/1.0.0/download" hash: type: string description: This is generated serverside + examples: + - "3c8d6d3d48967758055a5569a24617c3e6fdc456fbf6a4adbf1222954e61b634" + gd: + description: The GD version the mod supports (can be specified per platform) + oneOf: + - $ref: "#/components/schemas/GDVersionString" + - $ref: "#/components/schemas/GDVersionObject" mod_id: $ref: '#/components/schemas/ModID' @@ -302,15 +350,17 @@ components: Page: name: page in: query - description: Page number (default 1) + description: Page number required: false + example: 1 schema: type: integer PerPage: name: per_page in: query - description: Number of elements to fetch per page (default 10) + description: Number of elements to fetch per page required: false + example: 10 schema: type: integer diff --git a/src/auth/github.rs b/src/auth/github.rs new file mode 100644 index 0000000..114f85c --- /dev/null +++ b/src/auth/github.rs @@ -0,0 +1,145 @@ +use reqwest::{header::{HeaderMap, HeaderValue}, Client, StatusCode}; +use serde::{Deserialize, Serialize}; +use sqlx::{types::ipnetwork::Ipv4Network, PgConnection}; + +use crate::types::{api::ApiError, models::github_login_attempt::GithubLoginAttempt}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct GithubStartAuth { + device_code: String, + user_code: String, + verification_uri: String, + expires_in: i32, + interval: i32 +} + +pub struct GithubClient { + client_id: String, + client_secret: String +} + +impl GithubClient { + pub fn new(client_id: String, client_secret: String) -> GithubClient { + GithubClient {client_id, client_secret} + } + + pub async fn start_auth(&self, ip: Ipv4Network, pool: &mut PgConnection) -> Result { + #[derive(Serialize)] + struct GithubStartAuthBody { + client_id: String + } + let found_request = GithubLoginAttempt::get_one_by_ip(sqlx::types::ipnetwork::IpNetwork::V4(ip), &mut *pool).await?; + if found_request.is_some() { + return Err(ApiError::BadRequest("Login attempt already running".to_string())); + } + let mut headers = HeaderMap::new(); + headers.insert("Accept", HeaderValue::from_static("application/json")); + let client = Client::builder() + .default_headers(headers) + .build(); + if client.is_err() { + log::error!("{}", client.err().unwrap()); + return Err(ApiError::InternalError); + } + let client = client.unwrap(); + let body = GithubStartAuthBody {client_id: String::from(&self.client_id)}; + let json = match serde_json::to_string(&body) { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::InternalError); + }, + Ok(j) => j + }; + let result = client.execute( + client.post("https://github.com/login/device/code") + .basic_auth(&self.client_id, Some(&self.client_secret)) + .body(json) + .build().or(Err(ApiError::InternalError))? + ).await; + if result.is_err() { + log::error!("{}", result.err().unwrap()); + return Err(ApiError::InternalError); + } + + let result = result.unwrap(); + if result.status() != StatusCode::OK { + log::error!("Couldn't connect to GitHub"); + return Err(ApiError::InternalError); + } + let body = result.json::().await.or(Err(ApiError::InternalError))?; + let uuid = GithubLoginAttempt::create(sqlx::types::ipnetwork::IpNetwork::V4(ip), body.device_code, body.interval, body.expires_in, &mut *pool).await?; + + Ok(GithubLoginAttempt { uuid: uuid.to_string(), interval: body.interval, uri: body.verification_uri, code: body.user_code }) + } + + pub async fn poll_github(&self, device_code: &str) -> Result { + #[derive(Serialize, Debug)] + struct GithubPollAuthBody { + client_id: String, + device_code: String, + grant_type: String + } + let body = GithubPollAuthBody { + client_id: String::from(&self.client_id), + device_code: String::from(device_code), + grant_type: String::from("urn:ietf:params:oauth:grant-type:device_code") + }; + log::info!("{:?}", body); + let json = match serde_json::to_string(&body) { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::InternalError); + }, + Ok(j) => j + }; + let client = Client::new(); + let resp = client.post("https://github.com/login/oauth/access_token") + .header("Accept", HeaderValue::from_str("application/json").unwrap()) + .header("Content-Type", HeaderValue::from_str("application/json").unwrap()) + .basic_auth(&self.client_id, Some(&self.client_secret)) + .body(json) + .send() + .await; + if resp.is_err() { + log::info!("{}", resp.err().unwrap()); + return Err(ApiError::InternalError); + } + let resp = resp.unwrap(); + let body = resp.json::().await.unwrap(); + match body.get("access_token") { + None => { + return Err(ApiError::BadRequest("Request not accepted by user".to_string())); + }, + Some(t) => Ok(String::from(t.as_str().unwrap())) + } + } + + pub async fn get_user(&self, token: String) -> Result { + let client = Client::new(); + let resp = client.get("https://api.github.com/user") + .header("Accept", HeaderValue::from_str("application/json").unwrap()) + .header("User-Agent", "geode_index") + .bearer_auth(token) + .send() + .await; + + if resp.is_err() { + log::info!("{}", resp.err().unwrap()); + return Err(ApiError::InternalError); + } + + let resp = resp.unwrap(); + if !resp.status().is_success() { + return Err(ApiError::InternalError); + } + let body = match resp.json::().await { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::InternalError); + }, + Ok(b) => b + }; + + return Ok(body); + } +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..bcd896d --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,2 @@ +pub mod github; +pub mod token; \ No newline at end of file diff --git a/src/auth/token.rs b/src/auth/token.rs new file mode 100644 index 0000000..3add25f --- /dev/null +++ b/src/auth/token.rs @@ -0,0 +1,19 @@ +use sqlx::PgConnection; +use uuid::Uuid; + +use crate::types::api::ApiError; + +pub async fn create_token_for_developer(id: i32, pool: &mut PgConnection) -> Result { + let result = sqlx::query!("INSERT INTO auth_tokens (developer_id) VALUES ($1) returning token", id) + .fetch_one(&mut *pool) + .await; + let result = match result { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + }, + Ok(r) => r + }; + + Ok(result.token) +} \ No newline at end of file diff --git a/src/endpoints/auth/github.rs b/src/endpoints/auth/github.rs new file mode 100644 index 0000000..257bfcd --- /dev/null +++ b/src/endpoints/auth/github.rs @@ -0,0 +1,95 @@ +use actix_web::{post, web, HttpRequest, Responder}; +use serde::Deserialize; +use sqlx::types::ipnetwork::Ipv4Network; +use uuid::Uuid; + +use crate::{ + auth::{ + github, token::create_token_for_developer + }, types::{ + api::{ + ApiError, ApiResponse + }, models::{ + developer::Developer, github_login_attempt::GithubLoginAttempt + } + }, AppData +}; + +#[derive(Deserialize)] +struct PollParams { + uuid: String +} + +#[post("v1/login/github")] +pub async fn start_github_login(data: web::Data, req: HttpRequest) -> Result { + let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; + let client = github::GithubClient::new(data.github_client_id.to_string(), data.github_client_secret.to_string()); + let connection_info = req.connection_info(); + let ip = match connection_info.realip_remote_addr() { + None => return Err(ApiError::InternalError), + Some(i) => i + }; + log::info!("{}", ip); + let net: Ipv4Network = ip.parse().or(Err(ApiError::InternalError))?; + + let result = client.start_auth(net, &mut *pool).await?; + Ok(web::Json(ApiResponse {error: "".to_string(), payload: result})) +} + +#[post("v1/login/github/poll")] +pub async fn poll_github_login(json: web::Json, data: web::Data, req: HttpRequest) -> Result { + let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; + let uuid = match Uuid::parse_str(&json.uuid) { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::BadRequest(format!("Invalid uuid {}", json.uuid))); + }, + Ok(u) => u + }; + let attempt = match GithubLoginAttempt::get_one(uuid, &mut *pool).await? { + None => { + return Err(ApiError::BadRequest(format!("No attempt made for uuid {}", json.uuid))) + }, + Some(a) => a + }; + + let connection_info = req.connection_info(); + let ip = match connection_info.realip_remote_addr() { + None => return Err(ApiError::InternalError), + Some(i) => i + }; + let net: Ipv4Network = ip.parse().or(Err(ApiError::InternalError))?; + if attempt.ip.ip() != net.ip() { + log::error!("{} compared to {}", attempt.ip, net); + return Err(ApiError::BadRequest("Request IP does not match stored attempt IP".to_string())); + } + if !attempt.interval_passed() { + return Err(ApiError::BadRequest("Too fast".to_string())); + } + if attempt.is_expired() { + GithubLoginAttempt::remove(uuid, &mut *pool).await; + return Err(ApiError::BadRequest("Login attempt expired".to_string())); + } + + let client = github::GithubClient::new(data.github_client_id.to_string(), data.github_client_secret.to_string()); + GithubLoginAttempt::poll(uuid, &mut *pool).await; + let token = client.poll_github(&attempt.device_code).await?; + GithubLoginAttempt::remove(uuid, &mut *pool).await; + let user = client.get_user(token).await?; + let id = match user.get("id") { + None => return Err(ApiError::InternalError), + Some(id) => id.as_i64().unwrap() + }; + if let Some(x) = Developer::get_by_github_id(id, &mut *pool).await? { + let token = create_token_for_developer(x.id, &mut *pool).await?; + return Ok(web::Json(ApiResponse {error: "".to_string(), payload: token.to_string()})); + } + let username = match user.get("login") { + None => return Err(ApiError::InternalError), + Some(user) => user.to_string() + }; + let id = Developer::create(id, username, &mut *pool).await?; + let token = create_token_for_developer(id, &mut *pool).await?; + + Ok(web::Json(ApiResponse {error: "".to_string(), payload: token.to_string()})) +} \ No newline at end of file diff --git a/src/endpoints/auth/mod.rs b/src/endpoints/auth/mod.rs new file mode 100644 index 0000000..b949282 --- /dev/null +++ b/src/endpoints/auth/mod.rs @@ -0,0 +1 @@ +pub mod github; \ No newline at end of file diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 6948dac..df7a841 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -1,2 +1,3 @@ pub mod mods; -pub mod mod_versions; \ No newline at end of file +pub mod mod_versions; +pub mod auth; \ No newline at end of file diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 1e4823c..1c38a3c 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -1,8 +1,8 @@ -use actix_web::{get, post, web, Responder, HttpResponse}; +use actix_web::{get, post, web, put, HttpResponse, Responder}; use serde::Deserialize; use sqlx::Acquire; -use crate::{AppData, types::{api::{ApiError, ApiResponse}, models::{mod_version::ModVersion, mod_entity::{download_geode_file, Mod}}, mod_json::ModJson}}; +use crate::{extractors::auth::Auth, types::{api::{ApiError, ApiResponse}, mod_json::ModJson, models::{mod_entity::{download_geode_file, Mod}, mod_version::ModVersion}}, AppData}; #[derive(Deserialize)] pub struct GetOnePath { @@ -15,11 +15,23 @@ pub struct CreateQueryParams { download_url: String } +#[derive(Deserialize)] +struct UpdatePayload { + validated: Option, + unlisted: Option +} + #[derive(Deserialize)] pub struct CreateVersionPath { id: String } +#[derive(Deserialize)] +struct UpdateVersionPath { + id: String, + version: String +} + #[get("v1/mods/{id}/versions/{version}")] pub async fn get_one(path: web::Path, data: web::Data) -> Result { let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; @@ -50,5 +62,32 @@ pub async fn create_version(path: web::Path, data: web::Data< return Err(result.err().unwrap()); } let _ = transaction.commit().await; + Ok(HttpResponse::NoContent()) +} + +#[put("v1/mods/{id}/versions/{version}")] +pub async fn update_version( + path: web::Path, + data: web::Data, + payload: web::Json, + auth: Auth +) -> Result { + if !auth.developer.admin { + return Err(ApiError::Forbidden); + } + let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; + let mut transaction = pool.begin().await.or(Err(ApiError::DbError))?; + let r = ModVersion::update_version(&path.id, &path.version, payload.validated, payload.unlisted, &mut *transaction).await; + if r.is_err() { + transaction.rollback().await.or(Err(ApiError::DbError))?; + return Err(r.err().unwrap()); + } + let r = Mod::try_update_latest_version(&path.id, &mut *transaction).await; + if r.is_err() { + transaction.rollback().await.or(Err(ApiError::DbError))?; + return Err(r.err().unwrap()); + } + transaction.commit().await.or(Err(ApiError::DbError))?; + Ok(HttpResponse::NoContent()) } \ No newline at end of file diff --git a/src/extractors/auth.rs b/src/extractors/auth.rs new file mode 100644 index 0000000..dc2ef9c --- /dev/null +++ b/src/extractors/auth.rs @@ -0,0 +1,64 @@ +use std::pin::Pin; + +use actix_web::{web, FromRequest, HttpRequest}; +use futures::Future; +use uuid::Uuid; + +use crate::{types::{api::ApiError, models::developer::FetchedDeveloper}, AppData}; + +pub struct Auth { + pub developer: FetchedDeveloper +} + +impl FromRequest for Auth { + type Error = ApiError; + type Future = Pin>>>; + + fn from_request(req: &HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future { + let data = req.app_data::>().unwrap().clone(); + let headers = req.headers().clone(); + Box::pin(async move { + let token = match headers.get("Authorization") { + None => { return Err(ApiError::Unauthorized) }, + Some(t) => match t.to_str() { + Err(e) => { + log::error!("Failed to parse auth token: {}", e); + return Err(ApiError::Unauthorized); + }, + Ok(str) => { + let split = str.split(" ").collect::>(); + if split.len() != 2 || split[0] != "Bearer" { + return Err(ApiError::Unauthorized); + } + match Uuid::try_parse(split[1]) { + Err(e) => { + log::error!("Failed to parse auth token {}, error: {}", str, e); + return Err(ApiError::Unauthorized); + }, + Ok(token) => token + } + } + } + }; + + let mut pool = data.db.acquire().await.or(Err(ApiError::DbAcquireError))?; + let developer = sqlx::query_as!(FetchedDeveloper, + "SELECT d.id, d.username, d.display_name, d.verified, d.admin FROM developers d + INNER JOIN auth_tokens at ON at.developer_id = d.id + WHERE at.token = $1", token + ).fetch_optional(&mut *pool).await; + let developer = match developer { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + }, + Ok(d) => match d { + None => return Err(ApiError::Unauthorized), + Some(data) => data + } + }; + + Ok(Auth { developer }) + }) + } +} \ No newline at end of file diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs new file mode 100644 index 0000000..5696e21 --- /dev/null +++ b/src/extractors/mod.rs @@ -0,0 +1 @@ +pub mod auth; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index b068669..da1f617 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,11 +7,15 @@ use crate::types::api; mod endpoints; mod types; +mod auth; +mod extractors; pub struct AppData { db: sqlx::postgres::PgPool, debug: bool, - app_url: String + app_url: String, + github_client_id: String, + github_client_secret: String } #[get("/")] @@ -39,11 +43,13 @@ async fn main() -> anyhow::Result<()> { let port = dotenvy::var("PORT").map_or(8080, |x: String| x.parse::().unwrap()); let debug = dotenvy::var("APP_DEBUG").unwrap_or("0".to_string()) == "1"; let app_url = dotenvy::var("APP_URL").unwrap_or("http://localhost".to_string()); + let github_client = dotenvy::var("GITHUB_CLIENT_ID").unwrap_or("".to_string()); + let github_secret = dotenvy::var("GITHUB_CLIENT_SECRET").unwrap_or("".to_string()); info!("Starting server on {}:{}", addr, port); let server = HttpServer::new(move || { App::new() - .app_data(web::Data::new(AppData { db: pool.clone(), debug, app_url: app_url.clone() })) + .app_data(web::Data::new(AppData { db: pool.clone(), debug, app_url: app_url.clone(), github_client_id: github_client.clone(), github_client_secret: github_secret.clone() })) .app_data(QueryConfig::default().error_handler(api::query_error_handler)) .wrap(Logger::default()) .service(endpoints::mods::index) @@ -52,6 +58,9 @@ async fn main() -> anyhow::Result<()> { .service(endpoints::mod_versions::get_one) .service(endpoints::mod_versions::download_version) .service(endpoints::mod_versions::create_version) + .service(endpoints::mod_versions::update_version) + .service(endpoints::auth::github::poll_github_login) + .service(endpoints::auth::github::start_github_login) .service(health) }).bind((addr, port))?; diff --git a/src/types/api.rs b/src/types/api.rs index 946cdf6..77aba25 100644 --- a/src/types/api.rs +++ b/src/types/api.rs @@ -16,7 +16,9 @@ pub enum ApiError { DbError, InternalError, BadRequest(String), - NotFound(String) + NotFound(String), + Unauthorized, + Forbidden } #[derive(Debug, Serialize, Deserialize)] @@ -33,7 +35,9 @@ impl Display for ApiError { Self::DbError => write!(f, "Unknown database error"), Self::BadRequest(message) => write!(f, "{}", message), Self::NotFound(message) => write!(f, "{}", message), - Self::InternalError => write!(f, "{}", "Internal server error") + Self::InternalError => write!(f, "{}", "Internal server error"), + Self::Forbidden => write!(f, "You cannot perform this action"), + Self::Unauthorized => write!(f, "You need to be authenticated to perform this action") } } } @@ -51,7 +55,9 @@ impl actix_web::ResponseError for ApiError { Self::DbError => StatusCode::INTERNAL_SERVER_ERROR, Self::BadRequest(_) => StatusCode::BAD_REQUEST, Self::NotFound(_) => StatusCode::NOT_FOUND, - Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR + Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + Self::Unauthorized => StatusCode::UNAUTHORIZED, + Self::Forbidden => StatusCode::FORBIDDEN } } } diff --git a/src/types/mod_json.rs b/src/types/mod_json.rs index dbb0023..eace720 100644 --- a/src/types/mod_json.rs +++ b/src/types/mod_json.rs @@ -179,12 +179,12 @@ impl ModJson { // I am going to n+1 this, I am sorry, will optimize later for i in deps { - let (ver, compare) = match split_version_and_compare(i.version.as_str()) { + let (dependency_ver, compare) = match split_version_and_compare(i.version.as_str()) { Err(_) => return Err(ApiError::BadRequest(format!("Invalid semver {}", i.version))), Ok((ver, compare)) => (ver, compare) }; - let versions = sqlx::query!("SELECT id, version FROM mod_versions WHERE mod_id = $1", i.id) + let versions = sqlx::query!("SELECT id, version FROM mod_versions WHERE mod_id = $1 and validated = true", i.id) .fetch_all(&mut *pool) .await; let versions = match versions { @@ -194,13 +194,17 @@ impl ModJson { if versions.len() == 0 { return Err(ApiError::BadRequest(format!("Couldn't find dependency {} on the index", i.id))); } + let mut found = false; for j in versions { // This should never fail (I hope) let parsed = semver::Version::parse(&j.version).unwrap(); - if compare_versions(&ver, &parsed, &compare) { + if compare_versions(&parsed, &dependency_ver, &compare) { ret.push(DependencyCreate { dependency_id: j.id, compare, importance: i.importance }); - continue; + found = true; + break; } + } + if !found { return Err(ApiError::BadRequest(format!("Couldn't find dependency version that satisfies semver compare {}", i.version))); } } @@ -235,13 +239,17 @@ impl ModJson { if versions.len() == 0 { return Err(ApiError::BadRequest(format!("Couldn't find incompatibility {} on the index", i.id))); } + let mut found = false; for j in versions { // This should never fail (I hope) let parsed = semver::Version::parse(&j.version).unwrap(); - if compare_versions(&ver, &parsed, &compare) { + if compare_versions(&parsed, &ver, &compare) { ret.push(IncompatibilityCreate { incompatibility_id: j.id, compare, importance: i.importance }); - continue; + found = true; + break; } + } + if !found { return Err(ApiError::BadRequest(format!("Couldn't find incompatibility version that satisfies semver compare {}", i.version))); } } @@ -292,7 +300,7 @@ fn validate_dependency_version_str(ver: &str) -> bool { fn split_version_and_compare(ver: &str) -> Result<(Version, ModVersionCompare), ()> { let mut copy = ver.to_string(); - let mut compare = ModVersionCompare::Exact; + let mut compare = ModVersionCompare::MoreEq; if ver.starts_with("<=") { copy = copy.trim_start_matches("<=").to_string(); compare = ModVersionCompare::LessEq; diff --git a/src/types/models/dependency.rs b/src/types/models/dependency.rs index cd05c48..87426bf 100644 --- a/src/types/models/dependency.rs +++ b/src/types/models/dependency.rs @@ -114,7 +114,7 @@ impl Dependency { } let mut builder: QueryBuilder = QueryBuilder::new("SELECT dp.dependency_id, dp.compare, dp.importance, mv.version, mv.mod_id FROM dependencies dp INNER JOIN mod_versions mv ON dp.dependency_id = mv.id - WHERE dp.dependent_id IN ("); + WHERE mv.validated = true AND dp.dependent_id IN ("); let mut separated = builder.separated(","); let copy = ret.clone(); for i in &modifiable_ids { @@ -125,7 +125,7 @@ impl Dependency { .fetch_all(&mut *pool) .await; if result.is_err() { - log::info!("{}", result.err().unwrap()); + log::error!("{}", result.err().unwrap()); return Err(ApiError::DbError); } let result = result.unwrap(); diff --git a/src/types/models/developer.rs b/src/types/models/developer.rs new file mode 100644 index 0000000..ea9e5c2 --- /dev/null +++ b/src/types/models/developer.rs @@ -0,0 +1,55 @@ +use sqlx::PgConnection; + +use crate::types::api::ApiError; + +pub struct Developer { + pub id: i32, + pub username: String, + pub display_name: String, +} + +pub struct FetchedDeveloper { + pub id: i32, + pub username: String, + pub display_name: String, + pub verified: bool, + pub admin: bool +} + +impl Developer { + pub async fn create(github_id: i64, username: String, pool: &mut PgConnection) -> Result { + let result = sqlx::query!( + "INSERT INTO developers + (username, display_name, github_user_id) VALUES + ($1, $2, $3) RETURNING id", + username, + username, + github_id + ).fetch_one(&mut *pool).await; + let id = match result { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + }, + Ok(row) => row.id + }; + Ok(id) + } + + pub async fn get_by_github_id(github_id: i64, pool: &mut PgConnection) -> Result, ApiError> { + let result = sqlx::query_as!( + Developer, + "SELECT id, username, display_name + FROM developers WHERE github_user_id = $1", + github_id + ).fetch_optional(&mut *pool).await; + + match result { + Err(e) => { + log::info!("{}", e); + return Err(ApiError::DbError); + }, + Ok(r) => Ok(r) + } + } +} \ No newline at end of file diff --git a/src/types/models/github_login_attempt.rs b/src/types/models/github_login_attempt.rs new file mode 100644 index 0000000..61f4e71 --- /dev/null +++ b/src/types/models/github_login_attempt.rs @@ -0,0 +1,110 @@ +use std::time::Duration; + +use serde::Serialize; +use sqlx::{types::{chrono::{DateTime, Utc}, ipnetwork::IpNetwork}, PgConnection}; +use uuid::Uuid; + +use crate::types::api::ApiError; + +#[derive(Serialize)] +pub struct GithubLoginAttempt { + pub uuid: String, + pub interval: i32, + pub uri: String, + pub code: String +} + +pub struct StoredLoginAttempt { + pub uuid: String, + pub ip: IpNetwork, + pub device_code: String, + pub interval: i32, + pub expires_in: i32, + pub created_at: DateTime, + pub last_poll: DateTime +} + +impl StoredLoginAttempt { + pub fn is_expired(&self) -> bool { + let now = Utc::now(); + let exprire_time = self.created_at + Duration::from_secs(u64::try_from(self.expires_in).unwrap()); + now > exprire_time + } + + pub fn interval_passed(&self) -> bool { + let now = Utc::now(); + let diff = (now - self.last_poll).num_seconds() as i32; + diff > self.interval + } +} + +impl GithubLoginAttempt { + pub async fn create( + ip: IpNetwork, + device_code: String, + interval: i32, + expires_in: i32, + pool: &mut PgConnection + ) -> Result{ + let result = sqlx::query!(" + INSERT INTO github_login_attempts + (ip, device_code, interval, expires_in) VALUES + ($1, $2, $3, $4) RETURNING uid + ", + ip, device_code, interval, expires_in) + .fetch_one(&mut *pool) + .await; + match result { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + }, + Ok(u) => Ok(u.uid) + } + } + + pub async fn get_one(uuid: Uuid, pool: &mut PgConnection) -> Result, ApiError> { + let result = sqlx::query_as!(StoredLoginAttempt, + "SELECT uid as uuid, ip, device_code, interval, expires_in, created_at, last_poll + FROM github_login_attempts + WHERE uid = $1", uuid + ).fetch_optional(pool).await; + + match result { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + }, + Ok(r) => Ok(r) + } + } + + pub async fn get_one_by_ip(ip: IpNetwork, pool: &mut PgConnection) -> Result, ApiError> { + let result = sqlx::query_as!(StoredLoginAttempt, + "SELECT uid as uuid, ip, device_code, interval, expires_in, created_at, last_poll + FROM github_login_attempts + WHERE ip = $1", ip + ).fetch_optional(pool).await; + + match result { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + }, + Ok(r) => Ok(r) + } + } + + pub async fn remove(uuid: Uuid, pool: &mut PgConnection) { + let _ = sqlx::query!("DELETE FROM github_login_attempts WHERE uid = $1", uuid) + .execute(&mut *pool) + .await; + } + + pub async fn poll(uuid: Uuid, pool: &mut PgConnection) { + let now = Utc::now(); + let _ = sqlx::query!("UPDATE github_login_attempts SET last_poll = $1 WHERE uid = $2", now, uuid) + .execute(&mut *pool) + .await; + } +} \ No newline at end of file diff --git a/src/types/models/incompatibility.rs b/src/types/models/incompatibility.rs index 6826a3f..0a5de6d 100644 --- a/src/types/models/incompatibility.rs +++ b/src/types/models/incompatibility.rs @@ -76,7 +76,7 @@ impl Incompatibility { icp.importance as "importance: IncompatibilityImportance", icp.incompatibility_id, mv.mod_id, mv.version FROM incompatibilities icp INNER JOIN mod_versions mv ON icp.mod_id = mv.id - WHERE mv.id = $1"#, id + WHERE mv.id = $1 AND mv.validated = true"#, id ).fetch_all(&mut *pool) .await; if result.is_err() { diff --git a/src/types/models/mod.rs b/src/types/models/mod.rs index 7fb58e0..7014fc1 100644 --- a/src/types/models/mod.rs +++ b/src/types/models/mod.rs @@ -2,4 +2,6 @@ pub mod mod_entity; pub mod mod_version; pub mod mod_gd_version; pub mod dependency; -pub mod incompatibility; \ No newline at end of file +pub mod incompatibility; +pub mod github_login_attempt; +pub mod developer; \ No newline at end of file diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index b3aae2b..12557b8 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -69,16 +69,16 @@ impl Mod { } } let mut builder: QueryBuilder = QueryBuilder::new( - "SELECT DISTINCT m.id, m.repository, m.latest_version, m.validated FROM mods m + "SELECT DISTINCT m.id, m.repository, m.latest_version, mv.validated, m.about, m.changelog FROM mods m INNER JOIN mod_versions mv ON m.id = mv.mod_id INNER JOIN mod_gd_versions mgv ON mgv.mod_id = mv.id - WHERE m.validated = true AND LOWER(mv.name) LIKE " + WHERE mv.validated = true AND LOWER(mv.name) LIKE " ); let mut counter_builder: QueryBuilder = QueryBuilder::new( - "SELECT COUNT(*) FROM mods m + "SELECT COUNT(DISTINCT m.id) FROM mods m INNER JOIN mod_versions mv ON m.id = mv.mod_id INNER JOIN mod_gd_versions mgv ON mgv.mod_id = mv.id - WHERE m.validated = true AND LOWER(mv.name) LIKE " + WHERE mv.validated = true AND LOWER(mv.name) LIKE " ); let query_string = format!("%{}%", query.query.unwrap_or("".to_string()).to_lowercase()); counter_builder.push_bind(&query_string); @@ -167,12 +167,12 @@ impl Mod { pub async fn get_one(id: &str, pool: &mut PgConnection) -> Result, ApiError> { let records: Vec = sqlx::query_as!(ModRecordGetOne, "SELECT - m.id, m.repository, m.latest_version, m.validated, m.about, m.changelog, + m.id, m.repository, m.latest_version, mv.validated, m.about, m.changelog, mv.id as version_id, mv.name, mv.description, mv.version, mv.download_link, mv.hash, mv.geode, mv.early_load, mv.api, mv.mod_id FROM mods m INNER JOIN mod_versions mv ON m.id = mv.mod_id - WHERE m.id = $1", + WHERE m.id = $1 AND mv.validated = true", id ).fetch_all(&mut *pool) .await @@ -192,7 +192,7 @@ impl Mod { early_load: x.early_load, api: x.api, mod_id: x.mod_id.clone(), - gd: DetailedGDVersion {win: None, android: None, mac: None, ios: None}, + gd: DetailedGDVersion { win: None, android: None, mac: None, ios: None, android32: None, android64: None }, dependencies: None, incompatibilities: None } @@ -234,38 +234,74 @@ impl Mod { } pub async fn new_version(json: &ModJson, pool: &mut PgConnection) -> Result<(), ApiError> { - let result = sqlx::query!("SELECT latest_version, validated, about, changelog FROM mods WHERE id = $1", json.id) + let result = sqlx::query!("SELECT DISTINCT m.id FROM mods m + INNER JOIN mod_versions mv ON mv.mod_id = m.id + WHERE m.id = $1 AND mv.validated = true", json.id) .fetch_optional(&mut *pool) .await .or(Err(ApiError::DbError))?; - let result = match result { - Some(r) => r, - None => return Err(ApiError::NotFound(format!("Mod {} doesn't exist", &json.id))) - }; - if !result.validated { - return Err(ApiError::BadRequest("Cannot update an unverified mod. Please contact the Geode team for more details.".into())); + if result.is_none() { + return Err(ApiError::NotFound(format!("Mod {} doesn't exist or isn't yet validated", &json.id))); } - let version = semver::Version::parse(result.latest_version.trim_start_matches("v")).unwrap(); + + let latest = sqlx::query!("SELECT mv.version, mv.id FROM mod_versions mv + INNER JOIN mods m ON mv.mod_id = m.id + WHERE m.id = $1 + ORDER BY mv.id DESC LIMIT 1", &json.id + ).fetch_one(&mut *pool).await.unwrap(); + + let version = semver::Version::parse(&latest.version.trim_start_matches("v")).unwrap(); let new_version = match semver::Version::parse(json.version.trim_start_matches("v")) { Ok(v) => v, Err(_) => return Err(ApiError::BadRequest(format!("Invalid semver {}", json.version))) }; if new_version.le(&version) { - return Err(ApiError::BadRequest(format!("mod.json version {} is smaller / equal to latest mod version {}", json.version, result.latest_version))); + return Err(ApiError::BadRequest(format!("mod.json version {} is smaller / equal to latest mod version {}", json.version, latest.version))); } ModVersion::create_from_json(json, pool).await?; - let result = sqlx::query!( - "UPDATE mods - SET latest_version = $1, changelog = $2, about = $3 - WHERE id = $4", json.version, json.changelog, json.about, json.id) + Ok(()) + } + + pub async fn try_update_latest_version(id: &str, pool: &mut PgConnection) -> Result<(), ApiError> { + let latest = sqlx::query!("SELECT mv.version, mv.id FROM mod_versions mv + INNER JOIN mods m ON mv.mod_id = m.id + WHERE m.id = $1 AND mv.validated = true + ORDER BY mv.id DESC LIMIT 1", id + ).fetch_optional(&mut *pool) + .await; + + let latest = match latest { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + }, + Ok(l) => l + }; + + if let None = latest { + return Ok(()); + } + + let latest = latest.unwrap(); + + let result = sqlx::query!("UPDATE mods SET latest_version = $1 WHERE id = $2", latest.version, id) .execute(&mut *pool) - .await - .or(Err(ApiError::DbError))?; - if result.rows_affected() == 0 { - log::error!("{:?}", result); - return Err(ApiError::DbError); + .await; + + match result { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + }, + Ok(r) => { + if r.rows_affected() == 0 { + log::info!("Something really bad happened with mod {}", id); + return Err(ApiError::InternalError); + } + + Ok(()) + } } - Ok(()) } async fn create(json: &ModJson, pool: &mut PgConnection) -> Result<(), ApiError> { @@ -286,7 +322,7 @@ impl Mod { if json.about.is_some() { query_builder.push("about, "); } - query_builder.push("id, latest_version, validated) VALUES ("); + query_builder.push("id, latest_version) VALUES ("); let mut separated = query_builder.separated(", "); if json.repository.is_some() { separated.push_bind(json.repository.as_ref().unwrap()); @@ -299,7 +335,6 @@ impl Mod { } separated.push_bind(&json.id); separated.push_bind(&json.version); - separated.push_bind(false); separated.push_unseparated(")"); let _ = query_builder diff --git a/src/types/models/mod_gd_version.rs b/src/types/models/mod_gd_version.rs index f111927..04a0ba9 100644 --- a/src/types/models/mod_gd_version.rs +++ b/src/types/models/mod_gd_version.rs @@ -73,7 +73,12 @@ pub struct ModGDVersionCreate { #[derive(Deserialize, Serialize, Debug, Clone)] pub struct DetailedGDVersion { pub win: Option, + #[serde(skip_serializing)] pub android: Option, + #[serde(skip_deserializing)] + pub android32: Option, + #[serde(skip_deserializing)] + pub android64: Option, pub mac: Option, pub ios: Option } @@ -111,7 +116,7 @@ impl DetailedGDVersion { impl Default for DetailedGDVersion { fn default() -> Self { - DetailedGDVersion { mac: None, ios: None, win: None, android: None } + DetailedGDVersion { mac: None, ios: None, win: None, android: None, android32: None, android64: None } } } @@ -188,10 +193,15 @@ impl ModGDVersion { }, Ok(r) => r }; - let mut ret = DetailedGDVersion { win: None, mac: None, android: None, ios: None }; + let mut ret = DetailedGDVersion { win: None, mac: None, android: None, ios: None, android32: None, android64: None }; for i in result { match i.platform { - VerPlatform::Android32 | VerPlatform::Android64 | VerPlatform::Android => { ret.android = Some(i.gd) }, + VerPlatform::Android32 => { ret.android32 = Some(i.gd) }, + VerPlatform::Android64 => { ret.android64 = Some(i.gd) }, + VerPlatform::Android => { + ret.android32 = Some(i.gd); + ret.android64 = Some(i.gd); + }, VerPlatform::Win => { ret.win = Some(i.gd) }, VerPlatform::Mac => { ret.mac = Some(i.gd) }, VerPlatform::Ios => { ret.ios = Some(i.gd) }, @@ -227,9 +237,14 @@ impl ModGDVersion { for i in result { match ret.entry(i.mod_id) { Entry::Vacant(e) => { - let mut ver = DetailedGDVersion::default(); + let mut ver = DetailedGDVersion::default(); match i.platform { - VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => ver.android = Some(i.gd), + VerPlatform::Android => { + ver.android32 = Some(i.gd); + ver.android64 = Some(i.gd); + }, + VerPlatform::Android32 => ver.android32 = Some(i.gd), + VerPlatform::Android64 => ver.android64 = Some(i.gd), VerPlatform::Mac => ver.mac = Some(i.gd), VerPlatform::Ios => ver.ios = Some(i.gd), VerPlatform::Win => ver.win = Some(i.gd) @@ -238,7 +253,12 @@ impl ModGDVersion { }, Entry::Occupied(mut e) => { match i.platform { - VerPlatform::Android | VerPlatform::Android32 | VerPlatform::Android64 => e.get_mut().android = Some(i.gd), + VerPlatform::Android => { + e.get_mut().android32 = Some(i.gd); + e.get_mut().android64 = Some(i.gd); + }, + VerPlatform::Android32 => e.get_mut().android32 = Some(i.gd), + VerPlatform::Android64 => e.get_mut().android64 = Some(i.gd), VerPlatform::Mac => e.get_mut().mac = Some(i.gd), VerPlatform::Ios => e.get_mut().ios = Some(i.gd), VerPlatform::Win => e.get_mut().win = Some(i.gd) diff --git a/src/types/models/mod_version.rs b/src/types/models/mod_version.rs index 0ee76c0..7521ee6 100644 --- a/src/types/models/mod_version.rs +++ b/src/types/models/mod_version.rs @@ -52,7 +52,7 @@ impl ModVersionGetOne { early_load: self.early_load, api: self.api, mod_id: self.mod_id.clone(), - gd: DetailedGDVersion {win: None, android: None, mac: None, ios: None}, + gd: DetailedGDVersion { win: None, android: None, mac: None, ios: None, android32: None, android64: None }, dependencies: None, incompatibilities: None } @@ -69,12 +69,12 @@ impl ModVersion { } let mut query_builder: QueryBuilder = QueryBuilder::new( - r#"SELECT + r#"SELECT DISTINCT mv.name, mv.id, mv.description, mv.version, mv.download_link, mv.hash, mv.geode, - mv.early_load, mv.api, mv.mod_id, m.changelog FROM mod_versions mv + mv.early_load, mv.api, mv.mod_id FROM mod_versions mv INNER JOIN mod_gd_versions mgv ON mgv.mod_id = mv.id INNER JOIN mods m ON m.id = mv.mod_id - WHERE mv.version = m.latest_version"# + WHERE mv.version = m.latest_version AND mv.validated = true"# ); if gd.is_some() { query_builder.push(" AND mgv.gd = "); @@ -125,7 +125,7 @@ impl ModVersion { } pub async fn get_download_url(id: &str, version: &str,pool: &mut PgConnection) -> Result { - let result = sqlx::query!("SELECT download_link FROM mod_versions WHERE mod_id = $1 AND version = $2", id, version) + let result = sqlx::query!("SELECT download_link FROM mod_versions WHERE mod_id = $1 AND version = $2 AND validated = true", id, version) .fetch_optional(&mut *pool) .await; if result.is_err() { @@ -143,7 +143,7 @@ impl ModVersion { if json.description.is_some() { builder.push("description, "); } - builder.push("name, version, download_link, hash, geode, early_load, api, mod_id) VALUES ("); + builder.push("name, version, download_link, validated, hash, geode, early_load, api, mod_id) VALUES ("); let mut separated = builder.separated(", "); if json.description.is_some() { separated.push_bind(&json.description); @@ -151,6 +151,7 @@ impl ModVersion { separated.push_bind(&json.name); separated.push_bind(&json.version); separated.push_bind(&json.download_url); + separated.push_bind(false); separated.push_bind(&json.hash); separated.push_bind(&json.geode); separated.push_bind(&json.early_load); @@ -193,9 +194,11 @@ impl ModVersion { pub async fn get_one(id: &str, version: &str, pool: &mut PgConnection) -> Result { let result = sqlx::query_as!( ModVersionGetOne, - "SELECT mv.* FROM mod_versions mv + "SELECT + mv.id, mv.name, mv.description, mv.version, mv.download_link, + mv.hash, mv.geode, mv.early_load, mv.api, mv.mod_id FROM mod_versions mv INNER JOIN mods m ON m.id = mv.mod_id - WHERE mv.mod_id = $1 AND mv.version = $2", + WHERE mv.mod_id = $1 AND mv.version = $2 AND mv.validated = true", id, version ).fetch_optional(&mut *pool) .await; @@ -232,4 +235,40 @@ impl ModVersion { Ok(version) } + + pub async fn update_version(id: &str, version: &str, validated: Option, unlisted: Option, pool: &mut PgConnection) -> Result<(), ApiError> { + if validated.is_none() && unlisted.is_none() { + return Ok(()); + } + + let mut query_builder : QueryBuilder = QueryBuilder::new("UPDATE mod_versions SET "); + + if let Some(v) = validated { + query_builder.push("validated = "); + query_builder.push_bind(v); + } + + query_builder.push("WHERE mod_id = "); + query_builder.push_bind(id); + query_builder.push(" AND version = "); + let version = version.trim_start_matches("v"); + query_builder.push_bind(version); + + let result = query_builder.build() + .execute(&mut *pool) + .await; + + match result { + Err(e) => { + log::error!("{}", e); + return Err(ApiError::DbError); + }, + Ok(r) => { + if r.rows_affected() == 0 { + return Err(ApiError::NotFound(format!("{} {} not found", id, version))); + } + Ok(()) + } + } + } } \ No newline at end of file