Switch to Trolley for Modrinth Payments (#727)
* most of trolley * Switch to trolley for payments * run prepare * fix clippy * fix more * Fix most tests + bitflags * Update src/auth/flows.rs Co-authored-by: Jackson Kruger <jak.kruger@gmail.com> * Finish trolley * run prep for merge * Update src/queue/payouts.rs Co-authored-by: Jackson Kruger <jak.kruger@gmail.com> --------- Co-authored-by: Jackson Kruger <jak.kruger@gmail.com>
This commit is contained in:
parent
f1ff88f452
commit
07ecd13554
6
.env
6
.env
@ -49,9 +49,9 @@ WHITELISTED_MODPACK_DOMAINS='["cdn.modrinth.com", "github.com", "raw.githubuserc
|
|||||||
|
|
||||||
ALLOWED_CALLBACK_URLS='["localhost", ".modrinth.com", "127.0.0.1"]'
|
ALLOWED_CALLBACK_URLS='["localhost", ".modrinth.com", "127.0.0.1"]'
|
||||||
|
|
||||||
PAYPAL_API_URL=https://api-m.sandbox.paypal.com/v1/
|
TROLLEY_ACCESS_KEY=none
|
||||||
PAYPAL_CLIENT_ID=none
|
TROLLEY_SECRET_KEY=none
|
||||||
PAYPAL_CLIENT_SECRET=none
|
TROLLEY_WEBHOOK_SIGNATURE=none
|
||||||
|
|
||||||
GITHUB_CLIENT_ID=none
|
GITHUB_CLIENT_ID=none
|
||||||
GITHUB_CLIENT_SECRET=none
|
GITHUB_CLIENT_SECRET=none
|
||||||
|
|||||||
4
.idea/labrinth.iml
generated
4
.idea/labrinth.iml
generated
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="CPP_MODULE" version="4">
|
<module type="CPP_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/ahash-7eac216debe9f38a/out" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/ahash-7eac216debe9f38a/out" isTestSource="false" />
|
||||||
@ -144,6 +145,7 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/v_htmlescape-2dcc3d8195b1decd/out" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/v_htmlescape-2dcc3d8195b1decd/out" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/value-bag-61e3e358d251e1a5/out" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/value-bag-61e3e358d251e1a5/out" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/whoami-7d9ebcbbb2371986/out" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/target/debug/build/whoami-7d9ebcbbb2371986/out" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
|||||||
1338
Cargo.lock
generated
1338
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -51,7 +51,7 @@ sha1 = { version = "0.6.1", features = ["std"] }
|
|||||||
sha2 = "0.9.9"
|
sha2 = "0.9.9"
|
||||||
hmac = "0.11.0"
|
hmac = "0.11.0"
|
||||||
argon2 = { version = "0.5.0", features = ["std"] }
|
argon2 = { version = "0.5.0", features = ["std"] }
|
||||||
bitflags = "1.3.2"
|
bitflags = "2.4.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
zxcvbn = "2.2.2"
|
zxcvbn = "2.2.2"
|
||||||
totp-rs = { version = "5.0.2", features = ["gen_secret"] }
|
totp-rs = { version = "5.0.2", features = ["gen_secret"] }
|
||||||
@ -96,7 +96,7 @@ maxminddb = "0.23.0"
|
|||||||
flate2 = "1.0.25"
|
flate2 = "1.0.25"
|
||||||
tar = "0.4.38"
|
tar = "0.4.38"
|
||||||
|
|
||||||
sentry = { version = "0.31.5", features = ["profiling"] }
|
sentry = { version = "0.31.5" }
|
||||||
sentry-actix = "0.31.5"
|
sentry-actix = "0.31.5"
|
||||||
|
|
||||||
image = "0.24.6"
|
image = "0.24.6"
|
||||||
|
|||||||
16
migrations/20230919183129_trolley.sql
Normal file
16
migrations/20230919183129_trolley.sql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN trolley_id text NULL,
|
||||||
|
ADD COLUMN trolley_account_status text NULL,
|
||||||
|
DROP COLUMN midas_expires,
|
||||||
|
DROP COLUMN is_overdue,
|
||||||
|
DROP COLUMN stripe_customer_id,
|
||||||
|
DROP COLUMN payout_wallet,
|
||||||
|
DROP COLUMN payout_wallet_type,
|
||||||
|
DROP COLUMN payout_address;
|
||||||
|
|
||||||
|
ALTER TABLE historical_payouts
|
||||||
|
ADD COLUMN batch_id text NULL,
|
||||||
|
ADD COLUMN payment_id text NULL;
|
||||||
|
|
||||||
|
UPDATE historical_payouts
|
||||||
|
SET status = 'processed'
|
||||||
440
sqlx-data.json
440
sqlx-data.json
@ -144,25 +144,25 @@
|
|||||||
},
|
},
|
||||||
"query": "\n UPDATE versions\n SET status = $1, date_published = $2\n WHERE (id = $3)\n "
|
"query": "\n UPDATE versions\n SET status = $1, date_published = $2\n WHERE (id = $3)\n "
|
||||||
},
|
},
|
||||||
"03284fe5b045e2cf93f160863c4d121439382b348b728fffb5ac588dee980731": {
|
"04128dd06489004e0d0305bfd0f4ca5ee4b4a6b9f610de6e1b9ef9c8543cc025": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"name": "exists",
|
"name": "id",
|
||||||
"ordinal": 0,
|
"ordinal": 0,
|
||||||
"type_info": "Bool"
|
"type_info": "Int8"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nullable": [
|
"nullable": [
|
||||||
null
|
false
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Int8"
|
"Text"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"query": "\n SELECT EXISTS(SELECT 1 FROM users WHERE id = $1 AND email IS NULL)\n "
|
"query": "SELECT id FROM users WHERE trolley_id = $1"
|
||||||
},
|
},
|
||||||
"04345d9c23430267f755b1420520df91bd403524fd60ba1a94e3a239ea70cae7": {
|
"04345d9c23430267f755b1420520df91bd403524fd60ba1a94e3a239ea70cae7": {
|
||||||
"describe": {
|
"describe": {
|
||||||
@ -1609,6 +1609,18 @@
|
|||||||
},
|
},
|
||||||
"query": "\n UPDATE mods\n SET follows = follows - 1\n WHERE id = $1\n "
|
"query": "\n UPDATE mods\n SET follows = follows - 1\n WHERE id = $1\n "
|
||||||
},
|
},
|
||||||
|
"382753714620109f2ad1a4cacbb6f699732db321a2dcb1f9d83e57332e32357d": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n UPDATE users\n SET trolley_account_status = NULL, trolley_id = NULL\n WHERE id = $1\n "
|
||||||
|
},
|
||||||
"38429340be03cc5f539d9d14c156e6b6710051d2826b53a5ccfdbd231af964ca": {
|
"38429340be03cc5f539d9d14c156e6b6710051d2826b53a5ccfdbd231af964ca": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
@ -1756,6 +1768,20 @@
|
|||||||
},
|
},
|
||||||
"query": "\n UPDATE mods\n SET title = $1\n WHERE (id = $2)\n "
|
"query": "\n UPDATE mods\n SET title = $1\n WHERE (id = $2)\n "
|
||||||
},
|
},
|
||||||
|
"3f525e05e94ccaea4abc059d54f48011517bd8997df0c7d42cc4caae62194ae6": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text",
|
||||||
|
"Text",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n UPDATE users\n SET trolley_id = $1, trolley_account_status = $2\n WHERE id = $3\n "
|
||||||
|
},
|
||||||
"40f7c5bec98fe3503d6bd6db2eae5a4edb8d5d6efda9b9dc124f344ae5c60e08": {
|
"40f7c5bec98fe3503d6bd6db2eae5a4edb8d5d6efda9b9dc124f344ae5c60e08": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -1986,20 +2012,6 @@
|
|||||||
},
|
},
|
||||||
"query": "\n INSERT INTO mods_donations (\n joining_mod_id, joining_platform_id, url\n )\n SELECT * FROM UNNEST($1::bigint[], $2::int[], $3::varchar[])\n "
|
"query": "\n INSERT INTO mods_donations (\n joining_mod_id, joining_platform_id, url\n )\n SELECT * FROM UNNEST($1::bigint[], $2::int[], $3::varchar[])\n "
|
||||||
},
|
},
|
||||||
"4778d2f5994fda2f978fa53e0840c1a9a2582ef0434a5ff7f21706f1dc4edcf4": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"nullable": [],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8",
|
|
||||||
"Numeric",
|
|
||||||
"Varchar"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n INSERT INTO historical_payouts (user_id, amount, status)\n VALUES ($1, $2, $3)\n "
|
|
||||||
},
|
|
||||||
"4838777a8ef4371f4f5bb4f4f038bb6d041455f0849a3972a5418d75165ae9c7": {
|
"4838777a8ef4371f4f5bb4f4f038bb6d041455f0849a3972a5418d75165ae9c7": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
@ -2032,6 +2044,22 @@
|
|||||||
},
|
},
|
||||||
"query": "\n SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n LEFT JOIN versions vd ON d.dependency_id = vd.id\n WHERE v.mod_id = $1\n "
|
"query": "\n SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n LEFT JOIN versions vd ON d.dependency_id = vd.id\n WHERE v.mod_id = $1\n "
|
||||||
},
|
},
|
||||||
|
"48dc011567c5d50ee734fd0bdd1f5d07d9ef066c485a9b34495120c9947489f8": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Numeric",
|
||||||
|
"Varchar",
|
||||||
|
"Text",
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n INSERT INTO historical_payouts (user_id, amount, status, batch_id, payment_id)\n VALUES ($1, $2, $3, $4, $5)\n "
|
||||||
|
},
|
||||||
"49813a96f007216072d69468aae705d73d5b85dcdd64a22060009b12d947ed5a": {
|
"49813a96f007216072d69468aae705d73d5b85dcdd64a22060009b12d947ed5a": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -2706,153 +2734,6 @@
|
|||||||
},
|
},
|
||||||
"query": "\n UPDATE mods_gallery\n SET ordering = $2\n WHERE id = $1\n "
|
"query": "\n UPDATE mods_gallery\n SET ordering = $2\n WHERE id = $1\n "
|
||||||
},
|
},
|
||||||
"60a251aea1efbc7d9357255e520f0ac13f3697fecb84b1e9edd5d9ea61fe0cb0": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "email",
|
|
||||||
"ordinal": 2,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "avatar_url",
|
|
||||||
"ordinal": 3,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "username",
|
|
||||||
"ordinal": 4,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "bio",
|
|
||||||
"ordinal": 5,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created",
|
|
||||||
"ordinal": 6,
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "role",
|
|
||||||
"ordinal": 7,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "badges",
|
|
||||||
"ordinal": 8,
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "balance",
|
|
||||||
"ordinal": 9,
|
|
||||||
"type_info": "Numeric"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "payout_wallet",
|
|
||||||
"ordinal": 10,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "payout_wallet_type",
|
|
||||||
"ordinal": 11,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "payout_address",
|
|
||||||
"ordinal": 12,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "github_id",
|
|
||||||
"ordinal": 13,
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "discord_id",
|
|
||||||
"ordinal": 14,
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "gitlab_id",
|
|
||||||
"ordinal": 15,
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "google_id",
|
|
||||||
"ordinal": 16,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "steam_id",
|
|
||||||
"ordinal": 17,
|
|
||||||
"type_info": "Int8"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "microsoft_id",
|
|
||||||
"ordinal": 18,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "email_verified",
|
|
||||||
"ordinal": 19,
|
|
||||||
"type_info": "Bool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "password",
|
|
||||||
"ordinal": 20,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "totp_secret",
|
|
||||||
"ordinal": 21,
|
|
||||||
"type_info": "Varchar"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8Array",
|
|
||||||
"TextArray"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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, totp_secret\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n "
|
|
||||||
},
|
|
||||||
"61a7f29e024bf2f1368370e3f6e8ef70317c7e8545b5b6d4235f21164948ba27": {
|
"61a7f29e024bf2f1368370e3f6e8ef70317c7e8545b5b6d4235f21164948ba27": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -3953,20 +3834,20 @@
|
|||||||
},
|
},
|
||||||
"query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id \n FROM versions v\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE\n WHERE v.id = $1\n "
|
"query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id \n FROM versions v\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE\n WHERE v.id = $1\n "
|
||||||
},
|
},
|
||||||
"8cbd74dad7a21128d99fd32b430c2e0427480f910e1f125ff56b893c67a6e8a4": {
|
"8f45a48700b8836f4ba8626b25b7be7f838d35d260430a46817729d9787e2013": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"nullable": [],
|
"nullable": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
"Varchar",
|
"Varchar",
|
||||||
"Varchar",
|
"Bool",
|
||||||
"Varchar",
|
"Text",
|
||||||
"Int8"
|
"Int8"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"query": "\n UPDATE users\n SET payout_wallet = $1, payout_wallet_type = $2, payout_address = $3\n WHERE (id = $4)\n "
|
"query": "\n UPDATE users\n SET email = $1, email_verified = $2, trolley_account_status = $3\n WHERE id = $4\n "
|
||||||
},
|
},
|
||||||
"8f5e2a570cf35b2d158182bac37fd40bcec277bbdeddaece5efaa88600048a70": {
|
"8f5e2a570cf35b2d158182bac37fd40bcec277bbdeddaece5efaa88600048a70": {
|
||||||
"describe": {
|
"describe": {
|
||||||
@ -4188,6 +4069,19 @@
|
|||||||
},
|
},
|
||||||
"query": "SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)"
|
"query": "SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)"
|
||||||
},
|
},
|
||||||
|
"9774f59e5d5ce6ba00ca7e3a4a81f80f78b908bdf664a4cdfad592a1b14c0d44": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Varchar",
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n UPDATE historical_payouts\n SET status = $1\n WHERE payment_id = $2\n "
|
||||||
|
},
|
||||||
"99a1eac69d7f5a5139703df431e6a5c3012a90143a8c635f93632f04d0bc41d4": {
|
"99a1eac69d7f5a5139703df431e6a5c3012a90143a8c635f93632f04d0bc41d4": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -5104,18 +4998,6 @@
|
|||||||
},
|
},
|
||||||
"query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.user_id = $1\n GROUP BY n.id, n.user_id;\n "
|
"query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.user_id = $1\n GROUP BY n.id, n.user_id;\n "
|
||||||
},
|
},
|
||||||
"c4b167ec7452cc92be0e33f7e4f3908f0c4109291511c94909e9105fc62a432f": {
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"nullable": [],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query": "\n UPDATE users\n SET payout_wallet = NULL, payout_wallet_type = NULL, payout_address = NULL\n WHERE (id = $1)\n "
|
|
||||||
},
|
|
||||||
"c55d2132e3e6e92dd50457affab758623dca175dc27a2d3cd4aace9cfdecf789": {
|
"c55d2132e3e6e92dd50457affab758623dca175dc27a2d3cd4aace9cfdecf789": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -6051,6 +5933,44 @@
|
|||||||
},
|
},
|
||||||
"query": "\n UPDATE versions\n SET featured = $1\n WHERE (id = $2)\n "
|
"query": "\n UPDATE versions\n SET featured = $1\n WHERE (id = $2)\n "
|
||||||
},
|
},
|
||||||
|
"e5adaf219c52ec828b72bd89c6b86a475f73181abf180a024dfe05f918e58edb": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "amount",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Numeric"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "SELECT id, amount, user_id, status FROM historical_payouts WHERE payment_id = $1"
|
||||||
|
},
|
||||||
"e60ea75112db37d3e73812e21b1907716e4762e06aa883af878e3be82e3f87d3": {
|
"e60ea75112db37d3e73812e21b1907716e4762e06aa883af878e3be82e3f87d3": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
@ -6417,6 +6337,19 @@
|
|||||||
},
|
},
|
||||||
"query": "\n INSERT INTO collections_mods (collection_id, mod_id)\n SELECT * FROM UNNEST($1::bigint[], $2::bigint[])\n ON CONFLICT DO NOTHING\n "
|
"query": "\n INSERT INTO collections_mods (collection_id, mod_id)\n SELECT * FROM UNNEST($1::bigint[], $2::bigint[])\n ON CONFLICT DO NOTHING\n "
|
||||||
},
|
},
|
||||||
|
"f141cc6711123b4fe5a5d9a7337a0b009b80e5d8fbda664b8d62b1a3f38eb936": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Numeric",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n UPDATE users\n SET balance = balance + $1\n WHERE id = $2\n "
|
||||||
|
},
|
||||||
"f1525930830e17b5ee8feb796d9950dd3741131965f050840fa75423b5a54f01": {
|
"f1525930830e17b5ee8feb796d9950dd3741131965f050840fa75423b5a54f01": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -6648,6 +6581,147 @@
|
|||||||
},
|
},
|
||||||
"query": "\n INSERT INTO game_versions_versions (game_version_id, joining_version_id)\n SELECT * FROM UNNEST($1::integer[], $2::bigint[])\n "
|
"query": "\n INSERT INTO game_versions_versions (game_version_id, joining_version_id)\n SELECT * FROM UNNEST($1::integer[], $2::bigint[])\n "
|
||||||
},
|
},
|
||||||
|
"faec0a606ccaeb3f21c81e60a1749640b929e97db40252118fb72610df64a457": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "avatar_url",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bio",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "role",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "badges",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "balance",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Numeric"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "github_id",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "discord_id",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gitlab_id",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "google_id",
|
||||||
|
"ordinal": 13,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "steam_id",
|
||||||
|
"ordinal": 14,
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "microsoft_id",
|
||||||
|
"ordinal": 15,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email_verified",
|
||||||
|
"ordinal": 16,
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"ordinal": 17,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "totp_secret",
|
||||||
|
"ordinal": 18,
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trolley_id",
|
||||||
|
"ordinal": 19,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trolley_account_status",
|
||||||
|
"ordinal": 20,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8Array",
|
||||||
|
"TextArray"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "\n SELECT id, name, email,\n avatar_url, username, bio,\n created, role, badges,\n balance,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, trolley_id, trolley_account_status\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n "
|
||||||
|
},
|
||||||
"fb955ca41b95120f66c98c0b528b1db10c4be4a55e9641bb104d772e390c9bb7": {
|
"fb955ca41b95120f66c98c0b528b1db10c4be4a55e9641bb104d772e390c9bb7": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|||||||
@ -8,7 +8,8 @@ use crate::file_hosting::FileHost;
|
|||||||
use crate::models::ids::base62_impl::{parse_base62, to_base62};
|
use crate::models::ids::base62_impl::{parse_base62, to_base62};
|
||||||
use crate::models::ids::random_base62_rng;
|
use crate::models::ids::random_base62_rng;
|
||||||
use crate::models::pats::Scopes;
|
use crate::models::pats::Scopes;
|
||||||
use crate::models::users::{Badges, Role};
|
use crate::models::users::{Badges, RecipientStatus, Role, UserPayoutData};
|
||||||
|
use crate::queue::payouts::{AccountUser, PayoutsQueue};
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
use crate::queue::socket::ActiveSockets;
|
use crate::queue::socket::ActiveSockets;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
@ -30,7 +31,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sqlx::postgres::PgPool;
|
use sqlx::postgres::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
pub fn config(cfg: &mut ServiceConfig) {
|
pub fn config(cfg: &mut ServiceConfig) {
|
||||||
@ -51,7 +52,8 @@ pub fn config(cfg: &mut ServiceConfig) {
|
|||||||
.service(resend_verify_email)
|
.service(resend_verify_email)
|
||||||
.service(set_email)
|
.service(set_email)
|
||||||
.service(verify_email)
|
.service(verify_email)
|
||||||
.service(subscribe_newsletter),
|
.service(subscribe_newsletter)
|
||||||
|
.service(link_trolley),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,9 +227,8 @@ impl TempUser {
|
|||||||
role: Role::Developer.to_string(),
|
role: Role::Developer.to_string(),
|
||||||
badges: Badges::default(),
|
badges: Badges::default(),
|
||||||
balance: Decimal::ZERO,
|
balance: Decimal::ZERO,
|
||||||
payout_wallet: None,
|
trolley_id: None,
|
||||||
payout_wallet_type: None,
|
trolley_account_status: None,
|
||||||
payout_address: None,
|
|
||||||
}
|
}
|
||||||
.insert(transaction)
|
.insert(transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@ -1013,7 +1014,7 @@ pub async fn auth_callback(
|
|||||||
|
|
||||||
let sockets = active_sockets.clone();
|
let sockets = active_sockets.clone();
|
||||||
let state = state_string.clone();
|
let state = state_string.clone();
|
||||||
let res: Result<HttpResponse, AuthenticationError> = (|| async move {
|
let res: Result<HttpResponse, AuthenticationError> = async move {
|
||||||
|
|
||||||
let flow = Flow::get(&state, &redis).await?;
|
let flow = Flow::get(&state, &redis).await?;
|
||||||
|
|
||||||
@ -1175,7 +1176,7 @@ pub async fn auth_callback(
|
|||||||
} else {
|
} else {
|
||||||
Err::<HttpResponse, AuthenticationError>(AuthenticationError::InvalidCredentials)
|
Err::<HttpResponse, AuthenticationError>(AuthenticationError::InvalidCredentials)
|
||||||
}
|
}
|
||||||
})().await;
|
}.await;
|
||||||
|
|
||||||
// Because this is callback route, if we have an error, we need to ensure we close the original socket if it exists
|
// Because this is callback route, if we have an error, we need to ensure we close the original socket if it exists
|
||||||
if let Err(ref e) = res {
|
if let Err(ref e) = res {
|
||||||
@ -1385,9 +1386,8 @@ pub async fn create_account_with_password(
|
|||||||
role: Role::Developer.to_string(),
|
role: Role::Developer.to_string(),
|
||||||
badges: Badges::default(),
|
badges: Badges::default(),
|
||||||
balance: Decimal::ZERO,
|
balance: Decimal::ZERO,
|
||||||
payout_wallet: None,
|
trolley_id: None,
|
||||||
payout_wallet_type: None,
|
trolley_account_status: None,
|
||||||
payout_address: None,
|
|
||||||
}
|
}
|
||||||
.insert(&mut transaction)
|
.insert(&mut transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@ -2011,6 +2011,7 @@ pub async fn set_email(
|
|||||||
redis: Data<RedisPool>,
|
redis: Data<RedisPool>,
|
||||||
email: web::Json<SetEmail>,
|
email: web::Json<SetEmail>,
|
||||||
session_queue: Data<AuthQueue>,
|
session_queue: Data<AuthQueue>,
|
||||||
|
payouts_queue: Data<Mutex<PayoutsQueue>>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
email
|
email
|
||||||
.0
|
.0
|
||||||
@ -2064,6 +2065,17 @@ pub async fn set_email(
|
|||||||
"We need to verify your email address.",
|
"We need to verify your email address.",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
if let Some(UserPayoutData {
|
||||||
|
trolley_id: Some(trolley_id),
|
||||||
|
..
|
||||||
|
}) = user.payout_data
|
||||||
|
{
|
||||||
|
let queue = payouts_queue.lock().await;
|
||||||
|
queue
|
||||||
|
.update_recipient_email(&trolley_id, &email.email)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?;
|
crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?;
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
@ -2206,3 +2218,59 @@ fn send_email_verify(
|
|||||||
Some(("Verify email", &format!("{}/{}?flow={}", dotenvy::var("SITE_URL")?, dotenvy::var("SITE_VERIFY_EMAIL_PATH")?, flow))),
|
Some(("Verify email", &format!("{}/{}?flow={}", dotenvy::var("SITE_URL")?, dotenvy::var("SITE_VERIFY_EMAIL_PATH")?, flow))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("trolley/link")]
|
||||||
|
pub async fn link_trolley(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: Data<PgPool>,
|
||||||
|
redis: Data<RedisPool>,
|
||||||
|
session_queue: Data<AuthQueue>,
|
||||||
|
payouts_queue: Data<Mutex<PayoutsQueue>>,
|
||||||
|
body: web::Json<AccountUser>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let user = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Some(&[Scopes::PAYOUTS_WRITE]),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.1;
|
||||||
|
|
||||||
|
if let Some(payout_data) = user.payout_data {
|
||||||
|
if payout_data.trolley_id.is_some() {
|
||||||
|
return Err(ApiError::InvalidInput(
|
||||||
|
"User already has a trolley account.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(email) = user.email {
|
||||||
|
let id = payouts_queue.lock().await.register_recipient(&email, body.0).await?;
|
||||||
|
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE users
|
||||||
|
SET trolley_id = $1, trolley_account_status = $2
|
||||||
|
WHERE id = $3
|
||||||
|
",
|
||||||
|
id,
|
||||||
|
RecipientStatus::Incomplete.as_str(),
|
||||||
|
user.id.0 as i64,
|
||||||
|
)
|
||||||
|
.execute(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::NoContent().finish())
|
||||||
|
} else {
|
||||||
|
Err(ApiError::InvalidInput(
|
||||||
|
"User needs to have an email set on account.".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -56,16 +56,15 @@ where
|
|||||||
created: db_user.created,
|
created: db_user.created,
|
||||||
role: Role::from_string(&db_user.role),
|
role: Role::from_string(&db_user.role),
|
||||||
badges: db_user.badges,
|
badges: db_user.badges,
|
||||||
payout_data: Some(UserPayoutData {
|
|
||||||
balance: db_user.balance,
|
|
||||||
payout_wallet: db_user.payout_wallet,
|
|
||||||
payout_wallet_type: db_user.payout_wallet_type,
|
|
||||||
payout_address: db_user.payout_address,
|
|
||||||
}),
|
|
||||||
auth_providers: Some(auth_providers),
|
auth_providers: Some(auth_providers),
|
||||||
has_password: Some(db_user.password.is_some()),
|
has_password: Some(db_user.password.is_some()),
|
||||||
has_totp: Some(db_user.totp_secret.is_some()),
|
has_totp: Some(db_user.totp_secret.is_some()),
|
||||||
github_id: None,
|
github_id: None,
|
||||||
|
payout_data: Some(UserPayoutData {
|
||||||
|
balance: db_user.balance,
|
||||||
|
trolley_id: db_user.trolley_id,
|
||||||
|
trolley_status: db_user.trolley_account_status,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(required_scopes) = required_scopes {
|
if let Some(required_scopes) = required_scopes {
|
||||||
|
|||||||
@ -76,10 +76,10 @@ pub async fn fetch_playtimes(
|
|||||||
.bind(start_date)
|
.bind(start_date)
|
||||||
.bind(end_date);
|
.bind(end_date);
|
||||||
|
|
||||||
if projects.is_some() {
|
if let Some(projects) = projects {
|
||||||
query = query.bind(projects.unwrap().iter().map(|x| x.0).collect::<Vec<_>>());
|
query = query.bind(projects.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||||
} else if versions.is_some() {
|
} else if let Some(versions) = versions {
|
||||||
query = query.bind(versions.unwrap().iter().map(|x| x.0).collect::<Vec<_>>());
|
query = query.bind(versions.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(query.fetch_all().await?)
|
Ok(query.fetch_all().await?)
|
||||||
@ -123,10 +123,10 @@ pub async fn fetch_views(
|
|||||||
.bind(start_date)
|
.bind(start_date)
|
||||||
.bind(end_date);
|
.bind(end_date);
|
||||||
|
|
||||||
if projects.is_some() {
|
if let Some(projects) = projects {
|
||||||
query = query.bind(projects.unwrap().iter().map(|x| x.0).collect::<Vec<_>>());
|
query = query.bind(projects.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||||
} else if versions.is_some() {
|
} else if let Some(versions) = versions {
|
||||||
query = query.bind(versions.unwrap().iter().map(|x| x.0).collect::<Vec<_>>());
|
query = query.bind(versions.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(query.fetch_all().await?)
|
Ok(query.fetch_all().await?)
|
||||||
@ -170,10 +170,10 @@ pub async fn fetch_downloads(
|
|||||||
.bind(start_date)
|
.bind(start_date)
|
||||||
.bind(end_date);
|
.bind(end_date);
|
||||||
|
|
||||||
if projects.is_some() {
|
if let Some(projects) = projects {
|
||||||
query = query.bind(projects.unwrap().iter().map(|x| x.0).collect::<Vec<_>>());
|
query = query.bind(projects.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||||
} else if versions.is_some() {
|
} else if let Some(versions) = versions {
|
||||||
query = query.bind(versions.unwrap().iter().map(|x| x.0).collect::<Vec<_>>());
|
query = query.bind(versions.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(query.fetch_all().await?)
|
Ok(query.fetch_all().await?)
|
||||||
@ -233,10 +233,10 @@ pub async fn fetch_countries(
|
|||||||
"
|
"
|
||||||
)).bind(start_date).bind(end_date).bind(start_date).bind(end_date);
|
)).bind(start_date).bind(end_date).bind(start_date).bind(end_date);
|
||||||
|
|
||||||
if projects.is_some() {
|
if let Some(projects) = projects {
|
||||||
query = query.bind(projects.unwrap().iter().map(|x| x.0).collect::<Vec<_>>());
|
query = query.bind(projects.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||||
} else if versions.is_some() {
|
} else if let Some(versions) = versions {
|
||||||
query = query.bind(versions.unwrap().iter().map(|x| x.0).collect::<Vec<_>>());
|
query = query.bind(versions.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(query.fetch_all().await?)
|
Ok(query.fetch_all().await?)
|
||||||
|
|||||||
@ -210,7 +210,7 @@ impl Collection {
|
|||||||
color: m.color.map(|x| x as u32),
|
color: m.color.map(|x| x as u32),
|
||||||
created: m.created,
|
created: m.created,
|
||||||
updated: m.updated,
|
updated: m.updated,
|
||||||
status: CollectionStatus::from_str(&m.status),
|
status: CollectionStatus::from_string(&m.status),
|
||||||
projects: m
|
projects: m
|
||||||
.mods
|
.mods
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
|||||||
@ -630,10 +630,10 @@ impl Project {
|
|||||||
license_url: m.license_url.clone(),
|
license_url: m.license_url.clone(),
|
||||||
discord_url: m.discord_url.clone(),
|
discord_url: m.discord_url.clone(),
|
||||||
client_side: SideTypeId(m.client_side),
|
client_side: SideTypeId(m.client_side),
|
||||||
status: ProjectStatus::from_str(
|
status: ProjectStatus::from_string(
|
||||||
&m.status,
|
&m.status,
|
||||||
),
|
),
|
||||||
requested_status: m.requested_status.map(|x| ProjectStatus::from_str(
|
requested_status: m.requested_status.map(|x| ProjectStatus::from_string(
|
||||||
&x,
|
&x,
|
||||||
)),
|
)),
|
||||||
server_side: SideTypeId(m.server_side),
|
server_side: SideTypeId(m.server_side),
|
||||||
@ -647,7 +647,7 @@ impl Project {
|
|||||||
webhook_sent: m.webhook_sent,
|
webhook_sent: m.webhook_sent,
|
||||||
color: m.color.map(|x| x as u32),
|
color: m.color.map(|x| x as u32),
|
||||||
queued: m.queued,
|
queued: m.queued,
|
||||||
monetization_status: MonetizationStatus::from_str(
|
monetization_status: MonetizationStatus::from_string(
|
||||||
&m.monetization_status,
|
&m.monetization_status,
|
||||||
),
|
),
|
||||||
loaders: m.loaders,
|
loaders: m.loaders,
|
||||||
@ -685,8 +685,8 @@ impl Project {
|
|||||||
donation_urls: serde_json::from_value(
|
donation_urls: serde_json::from_value(
|
||||||
m.donations.unwrap_or_default(),
|
m.donations.unwrap_or_default(),
|
||||||
).ok().unwrap_or_default(),
|
).ok().unwrap_or_default(),
|
||||||
client_side: crate::models::projects::SideType::from_str(&m.client_side_type),
|
client_side: crate::models::projects::SideType::from_string(&m.client_side_type),
|
||||||
server_side: crate::models::projects::SideType::from_str(&m.server_side_type),
|
server_side: crate::models::projects::SideType::from_string(&m.server_side_type),
|
||||||
thread_id: ThreadId(m.thread_id),
|
thread_id: ThreadId(m.thread_id),
|
||||||
}}))
|
}}))
|
||||||
})
|
})
|
||||||
|
|||||||
@ -148,7 +148,7 @@ impl Thread {
|
|||||||
id: ThreadId(x.id),
|
id: ThreadId(x.id),
|
||||||
project_id: x.mod_id.map(ProjectId),
|
project_id: x.mod_id.map(ProjectId),
|
||||||
report_id: x.report_id.map(ReportId),
|
report_id: x.report_id.map(ReportId),
|
||||||
type_: ThreadType::from_str(&x.thread_type),
|
type_: ThreadType::from_string(&x.thread_type),
|
||||||
messages: {
|
messages: {
|
||||||
let mut messages: Vec<ThreadMessage> = serde_json::from_value(
|
let mut messages: Vec<ThreadMessage> = serde_json::from_value(
|
||||||
x.messages.unwrap_or_default(),
|
x.messages.unwrap_or_default(),
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use super::CollectionId;
|
|||||||
use crate::database::models::DatabaseError;
|
use crate::database::models::DatabaseError;
|
||||||
use crate::database::redis::RedisPool;
|
use crate::database::redis::RedisPool;
|
||||||
use crate::models::ids::base62_impl::{parse_base62, to_base62};
|
use crate::models::ids::base62_impl::{parse_base62, to_base62};
|
||||||
use crate::models::users::{Badges, RecipientType, RecipientWallet};
|
use crate::models::users::{Badges, RecipientStatus};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -35,10 +35,10 @@ pub struct User {
|
|||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub badges: Badges,
|
pub badges: Badges,
|
||||||
|
|
||||||
pub balance: Decimal,
|
pub balance: Decimal,
|
||||||
pub payout_wallet: Option<RecipientWallet>,
|
pub trolley_id: Option<String>,
|
||||||
pub payout_wallet_type: Option<RecipientType>,
|
pub trolley_account_status: Option<RecipientStatus>,
|
||||||
pub payout_address: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
@ -188,9 +188,9 @@ impl User {
|
|||||||
SELECT id, name, email,
|
SELECT id, name, email,
|
||||||
avatar_url, username, bio,
|
avatar_url, username, bio,
|
||||||
created, role, badges,
|
created, role, badges,
|
||||||
balance, payout_wallet, payout_wallet_type, payout_address,
|
balance,
|
||||||
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
||||||
email_verified, password, totp_secret
|
email_verified, password, totp_secret, trolley_id, trolley_account_status
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = ANY($1) OR LOWER(username) = ANY($2)
|
WHERE id = ANY($1) OR LOWER(username) = ANY($2)
|
||||||
",
|
",
|
||||||
@ -220,13 +220,13 @@ impl User {
|
|||||||
role: u.role,
|
role: u.role,
|
||||||
badges: Badges::from_bits(u.badges as u64).unwrap_or_default(),
|
badges: Badges::from_bits(u.badges as u64).unwrap_or_default(),
|
||||||
balance: u.balance,
|
balance: u.balance,
|
||||||
payout_wallet: u.payout_wallet.map(|x| RecipientWallet::from_string(&x)),
|
|
||||||
payout_wallet_type: u
|
|
||||||
.payout_wallet_type
|
|
||||||
.map(|x| RecipientType::from_string(&x)),
|
|
||||||
payout_address: u.payout_address,
|
|
||||||
password: u.password,
|
password: u.password,
|
||||||
totp_secret: u.totp_secret,
|
totp_secret: u.totp_secret,
|
||||||
|
trolley_id: u.trolley_id,
|
||||||
|
trolley_account_status: u
|
||||||
|
.trolley_account_status
|
||||||
|
.as_ref()
|
||||||
|
.map(|x| RecipientStatus::from_string(x)),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<User>>()
|
.try_collect::<Vec<User>>()
|
||||||
@ -352,7 +352,7 @@ impl User {
|
|||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
) -> Result<(), DatabaseError> {
|
) -> Result<(), DatabaseError> {
|
||||||
redis
|
redis
|
||||||
.delete_many(user_ids.into_iter().flat_map(|(id, username)| {
|
.delete_many(user_ids.iter().flat_map(|(id, username)| {
|
||||||
[
|
[
|
||||||
(USERS_NAMESPACE, Some(id.0.to_string())),
|
(USERS_NAMESPACE, Some(id.0.to_string())),
|
||||||
(
|
(
|
||||||
|
|||||||
@ -590,9 +590,9 @@ impl Version {
|
|||||||
downloads: v.downloads,
|
downloads: v.downloads,
|
||||||
version_type: v.version_type,
|
version_type: v.version_type,
|
||||||
featured: v.featured,
|
featured: v.featured,
|
||||||
status: VersionStatus::from_str(&v.status),
|
status: VersionStatus::from_string(&v.status),
|
||||||
requested_status: v.requested_status
|
requested_status: v.requested_status
|
||||||
.map(|x| VersionStatus::from_str(&x)),
|
.map(|x| VersionStatus::from_string(&x)),
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -803,7 +803,7 @@ impl Version {
|
|||||||
.unwrap_or_default().into_iter().map(|x| (x.algorithm, x.hash)).collect(),
|
.unwrap_or_default().into_iter().map(|x| (x.algorithm, x.hash)).collect(),
|
||||||
primary: f.is_primary,
|
primary: f.is_primary,
|
||||||
size: f.size as u32,
|
size: f.size as u32,
|
||||||
file_type: f.file_type.map(|x| FileType::from_str(&x)),
|
file_type: f.file_type.map(|x| FileType::from_string(&x)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|||||||
@ -4,6 +4,7 @@ use bytes::Bytes;
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct MockHost(());
|
pub struct MockHost(());
|
||||||
|
|
||||||
impl MockHost {
|
impl MockHost {
|
||||||
|
|||||||
@ -365,9 +365,9 @@ pub fn check_env_vars() -> bool {
|
|||||||
failed |= true;
|
failed |= true;
|
||||||
}
|
}
|
||||||
|
|
||||||
failed |= check_var::<String>("PAYPAL_API_URL");
|
failed |= check_var::<String>("TROLLEY_ACCESS_KEY");
|
||||||
failed |= check_var::<String>("PAYPAL_CLIENT_ID");
|
failed |= check_var::<String>("TROLLEY_SECRET_KEY");
|
||||||
failed |= check_var::<String>("PAYPAL_CLIENT_SECRET");
|
failed |= check_var::<String>("TROLLEY_WEBHOOK_SIGNATURE");
|
||||||
|
|
||||||
failed |= check_var::<String>("GITHUB_CLIENT_ID");
|
failed |= check_var::<String>("GITHUB_CLIENT_ID");
|
||||||
failed |= check_var::<String>("GITHUB_CLIENT_SECRET");
|
failed |= check_var::<String>("GITHUB_CLIENT_SECRET");
|
||||||
|
|||||||
@ -31,8 +31,6 @@ async fn main() -> std::io::Result<()> {
|
|||||||
let sentry = sentry::init(sentry::ClientOptions {
|
let sentry = sentry::init(sentry::ClientOptions {
|
||||||
release: sentry::release_name!(),
|
release: sentry::release_name!(),
|
||||||
traces_sample_rate: 0.1,
|
traces_sample_rate: 0.1,
|
||||||
enable_profiling: true,
|
|
||||||
profiles_sample_rate: 0.1,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
if sentry.is_enabled() {
|
if sentry.is_enabled() {
|
||||||
|
|||||||
@ -80,7 +80,7 @@ impl std::fmt::Display for CollectionStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CollectionStatus {
|
impl CollectionStatus {
|
||||||
pub fn from_str(string: &str) -> CollectionStatus {
|
pub fn from_string(string: &str) -> CollectionStatus {
|
||||||
match string {
|
match string {
|
||||||
"listed" => CollectionStatus::Listed,
|
"listed" => CollectionStatus::Listed,
|
||||||
"unlisted" => CollectionStatus::Unlisted,
|
"unlisted" => CollectionStatus::Unlisted,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use super::ids::Base62Id;
|
use super::ids::Base62Id;
|
||||||
|
use crate::bitflags_serde_impl;
|
||||||
use crate::models::ids::UserId;
|
use crate::models::ids::UserId;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -10,8 +11,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub struct PatId(pub u64);
|
pub struct PatId(pub u64);
|
||||||
|
|
||||||
bitflags::bitflags! {
|
bitflags::bitflags! {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct Scopes: u64 {
|
pub struct Scopes: u64 {
|
||||||
// read a user's email
|
// read a user's email
|
||||||
const USER_READ_EMAIL = 1 << 0;
|
const USER_READ_EMAIL = 1 << 0;
|
||||||
@ -107,6 +107,8 @@ bitflags::bitflags! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bitflags_serde_impl!(Scopes, u64);
|
||||||
|
|
||||||
impl Scopes {
|
impl Scopes {
|
||||||
// these scopes cannot be specified in a personal access token
|
// these scopes cannot be specified in a personal access token
|
||||||
pub fn restricted() -> Scopes {
|
pub fn restricted() -> Scopes {
|
||||||
|
|||||||
@ -247,7 +247,7 @@ impl SideType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(string: &str) -> SideType {
|
pub fn from_string(string: &str) -> SideType {
|
||||||
match string {
|
match string {
|
||||||
"required" => SideType::Required,
|
"required" => SideType::Required,
|
||||||
"optional" => SideType::Optional,
|
"optional" => SideType::Optional,
|
||||||
@ -308,7 +308,7 @@ impl std::fmt::Display for ProjectStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectStatus {
|
impl ProjectStatus {
|
||||||
pub fn from_str(string: &str) -> ProjectStatus {
|
pub fn from_string(string: &str) -> ProjectStatus {
|
||||||
match string {
|
match string {
|
||||||
"processing" => ProjectStatus::Processing,
|
"processing" => ProjectStatus::Processing,
|
||||||
"rejected" => ProjectStatus::Rejected,
|
"rejected" => ProjectStatus::Rejected,
|
||||||
@ -433,7 +433,7 @@ impl std::fmt::Display for MonetizationStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MonetizationStatus {
|
impl MonetizationStatus {
|
||||||
pub fn from_str(string: &str) -> MonetizationStatus {
|
pub fn from_string(string: &str) -> MonetizationStatus {
|
||||||
match string {
|
match string {
|
||||||
"force-demonetized" => MonetizationStatus::ForceDemonetized,
|
"force-demonetized" => MonetizationStatus::ForceDemonetized,
|
||||||
"demonetized" => MonetizationStatus::Demonetized,
|
"demonetized" => MonetizationStatus::Demonetized,
|
||||||
@ -537,7 +537,7 @@ impl From<QueryVersion> for Version {
|
|||||||
version_id: d.version_id.map(|i| VersionId(i.0 as u64)),
|
version_id: d.version_id.map(|i| VersionId(i.0 as u64)),
|
||||||
project_id: d.project_id.map(|i| ProjectId(i.0 as u64)),
|
project_id: d.project_id.map(|i| ProjectId(i.0 as u64)),
|
||||||
file_name: d.file_name,
|
file_name: d.file_name,
|
||||||
dependency_type: DependencyType::from_str(d.dependency_type.as_str()),
|
dependency_type: DependencyType::from_string(d.dependency_type.as_str()),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
game_versions: data.game_versions.into_iter().map(GameVersion).collect(),
|
game_versions: data.game_versions.into_iter().map(GameVersion).collect(),
|
||||||
@ -570,7 +570,7 @@ impl std::fmt::Display for VersionStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VersionStatus {
|
impl VersionStatus {
|
||||||
pub fn from_str(string: &str) -> VersionStatus {
|
pub fn from_string(string: &str) -> VersionStatus {
|
||||||
match string {
|
match string {
|
||||||
"listed" => VersionStatus::Listed,
|
"listed" => VersionStatus::Listed,
|
||||||
"draft" => VersionStatus::Draft,
|
"draft" => VersionStatus::Draft,
|
||||||
@ -718,7 +718,7 @@ impl DependencyType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(string: &str) -> DependencyType {
|
pub fn from_string(string: &str) -> DependencyType {
|
||||||
match string {
|
match string {
|
||||||
"required" => DependencyType::Required,
|
"required" => DependencyType::Required,
|
||||||
"optional" => DependencyType::Optional,
|
"optional" => DependencyType::Optional,
|
||||||
@ -753,7 +753,7 @@ impl FileType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(string: &str) -> FileType {
|
pub fn from_string(string: &str) -> FileType {
|
||||||
match string {
|
match string {
|
||||||
"required-resource-pack" => FileType::RequiredResourcePack,
|
"required-resource-pack" => FileType::RequiredResourcePack,
|
||||||
"optional-resource-pack" => FileType::OptionalResourcePack,
|
"optional-resource-pack" => FileType::OptionalResourcePack,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use super::ids::Base62Id;
|
use super::ids::Base62Id;
|
||||||
|
use crate::bitflags_serde_impl;
|
||||||
use crate::models::users::User;
|
use crate::models::users::User;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -22,8 +23,7 @@ pub struct Team {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bitflags::bitflags! {
|
bitflags::bitflags! {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct ProjectPermissions: u64 {
|
pub struct ProjectPermissions: u64 {
|
||||||
const UPLOAD_VERSION = 1 << 0;
|
const UPLOAD_VERSION = 1 << 0;
|
||||||
const DELETE_VERSION = 1 << 1;
|
const DELETE_VERSION = 1 << 1;
|
||||||
@ -40,6 +40,8 @@ bitflags::bitflags! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bitflags_serde_impl!(ProjectPermissions, u64);
|
||||||
|
|
||||||
impl Default for ProjectPermissions {
|
impl Default for ProjectPermissions {
|
||||||
fn default() -> ProjectPermissions {
|
fn default() -> ProjectPermissions {
|
||||||
ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION
|
ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION
|
||||||
@ -77,8 +79,7 @@ impl ProjectPermissions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bitflags::bitflags! {
|
bitflags::bitflags! {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct OrganizationPermissions: u64 {
|
pub struct OrganizationPermissions: u64 {
|
||||||
const EDIT_DETAILS = 1 << 0;
|
const EDIT_DETAILS = 1 << 0;
|
||||||
const EDIT_BODY = 1 << 1;
|
const EDIT_BODY = 1 << 1;
|
||||||
@ -94,6 +95,8 @@ bitflags::bitflags! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bitflags_serde_impl!(OrganizationPermissions, u64);
|
||||||
|
|
||||||
impl Default for OrganizationPermissions {
|
impl Default for OrganizationPermissions {
|
||||||
fn default() -> OrganizationPermissions {
|
fn default() -> OrganizationPermissions {
|
||||||
OrganizationPermissions::NONE
|
OrganizationPermissions::NONE
|
||||||
|
|||||||
@ -78,7 +78,7 @@ impl ThreadType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(string: &str) -> ThreadType {
|
pub fn from_string(string: &str) -> ThreadType {
|
||||||
match string {
|
match string {
|
||||||
"report" => ThreadType::Report,
|
"report" => ThreadType::Report,
|
||||||
"project" => ThreadType::Project,
|
"project" => ThreadType::Project,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use super::ids::Base62Id;
|
use super::ids::Base62Id;
|
||||||
use crate::auth::flows::AuthProvider;
|
use crate::auth::flows::AuthProvider;
|
||||||
|
use crate::bitflags_serde_impl;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -12,8 +13,7 @@ pub struct UserId(pub u64);
|
|||||||
pub const DELETED_USER: UserId = UserId(127155982985829);
|
pub const DELETED_USER: UserId = UserId(127155982985829);
|
||||||
|
|
||||||
bitflags::bitflags! {
|
bitflags::bitflags! {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct Badges: u64 {
|
pub struct Badges: u64 {
|
||||||
// 1 << 0 unused - ignore + replace with something later
|
// 1 << 0 unused - ignore + replace with something later
|
||||||
const MIDAS = 1 << 0;
|
const MIDAS = 1 << 0;
|
||||||
@ -29,6 +29,8 @@ bitflags::bitflags! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bitflags_serde_impl!(Badges, u64);
|
||||||
|
|
||||||
impl Default for Badges {
|
impl Default for Badges {
|
||||||
fn default() -> Badges {
|
fn default() -> Badges {
|
||||||
Badges::NONE
|
Badges::NONE
|
||||||
@ -46,12 +48,12 @@ pub struct User {
|
|||||||
pub role: Role,
|
pub role: Role,
|
||||||
pub badges: Badges,
|
pub badges: Badges,
|
||||||
|
|
||||||
pub payout_data: Option<UserPayoutData>,
|
|
||||||
pub auth_providers: Option<Vec<AuthProvider>>,
|
pub auth_providers: Option<Vec<AuthProvider>>,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub email_verified: Option<bool>,
|
pub email_verified: Option<bool>,
|
||||||
pub has_password: Option<bool>,
|
pub has_password: Option<bool>,
|
||||||
pub has_totp: Option<bool>,
|
pub has_totp: Option<bool>,
|
||||||
|
pub payout_data: Option<UserPayoutData>,
|
||||||
|
|
||||||
// DEPRECATED. Always returns None
|
// DEPRECATED. Always returns None
|
||||||
pub github_id: Option<u64>,
|
pub github_id: Option<u64>,
|
||||||
@ -60,77 +62,8 @@ pub struct User {
|
|||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct UserPayoutData {
|
pub struct UserPayoutData {
|
||||||
pub balance: Decimal,
|
pub balance: Decimal,
|
||||||
pub payout_wallet: Option<RecipientWallet>,
|
pub trolley_id: Option<String>,
|
||||||
pub payout_wallet_type: Option<RecipientType>,
|
pub trolley_status: Option<RecipientStatus>,
|
||||||
pub payout_address: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum RecipientType {
|
|
||||||
Email,
|
|
||||||
Phone,
|
|
||||||
UserHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for RecipientType {
|
|
||||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
fmt.write_str(self.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecipientType {
|
|
||||||
pub fn from_string(string: &str) -> RecipientType {
|
|
||||||
match string {
|
|
||||||
"user_handle" => RecipientType::UserHandle,
|
|
||||||
"phone" => RecipientType::Phone,
|
|
||||||
_ => RecipientType::Email,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
RecipientType::Email => "email",
|
|
||||||
RecipientType::Phone => "phone",
|
|
||||||
RecipientType::UserHandle => "user_handle",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum RecipientWallet {
|
|
||||||
Venmo,
|
|
||||||
Paypal,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for RecipientWallet {
|
|
||||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
fmt.write_str(self.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecipientWallet {
|
|
||||||
pub fn from_string(string: &str) -> RecipientWallet {
|
|
||||||
match string {
|
|
||||||
"venmo" => RecipientWallet::Venmo,
|
|
||||||
_ => RecipientWallet::Paypal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
RecipientWallet::Paypal => "paypal",
|
|
||||||
RecipientWallet::Venmo => "venmo",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str_api(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
RecipientWallet::Paypal => "PayPal",
|
|
||||||
RecipientWallet::Venmo => "Venmo",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::database::models::user_item::User as DBUser;
|
use crate::database::models::user_item::User as DBUser;
|
||||||
@ -201,3 +134,89 @@ impl Role {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum RecipientStatus {
|
||||||
|
Active,
|
||||||
|
Incomplete,
|
||||||
|
Disabled,
|
||||||
|
Archived,
|
||||||
|
Suspended,
|
||||||
|
Blocked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecipientStatus {
|
||||||
|
pub fn from_string(string: &str) -> RecipientStatus {
|
||||||
|
match string {
|
||||||
|
"active" => RecipientStatus::Active,
|
||||||
|
"incomplete" => RecipientStatus::Incomplete,
|
||||||
|
"disabled" => RecipientStatus::Disabled,
|
||||||
|
"archived" => RecipientStatus::Archived,
|
||||||
|
"suspended" => RecipientStatus::Suspended,
|
||||||
|
"blocked" => RecipientStatus::Blocked,
|
||||||
|
_ => RecipientStatus::Disabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
RecipientStatus::Active => "active",
|
||||||
|
RecipientStatus::Incomplete => "incomplete",
|
||||||
|
RecipientStatus::Disabled => "disabled",
|
||||||
|
RecipientStatus::Archived => "archived",
|
||||||
|
RecipientStatus::Suspended => "suspended",
|
||||||
|
RecipientStatus::Blocked => "blocked",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Payout {
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub amount: Decimal,
|
||||||
|
pub status: PayoutStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum PayoutStatus {
|
||||||
|
Pending,
|
||||||
|
Failed,
|
||||||
|
Processed,
|
||||||
|
Returned,
|
||||||
|
Processing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PayoutStatus {
|
||||||
|
pub fn from_string(string: &str) -> PayoutStatus {
|
||||||
|
match string {
|
||||||
|
"pending" => PayoutStatus::Pending,
|
||||||
|
"failed" => PayoutStatus::Failed,
|
||||||
|
"processed" => PayoutStatus::Processed,
|
||||||
|
"returned" => PayoutStatus::Returned,
|
||||||
|
"processing" => PayoutStatus::Processing,
|
||||||
|
_ => PayoutStatus::Processing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PayoutStatus::Pending => "pending",
|
||||||
|
PayoutStatus::Failed => "failed",
|
||||||
|
PayoutStatus::Processed => "processed",
|
||||||
|
PayoutStatus::Returned => "returned",
|
||||||
|
PayoutStatus::Processing => "processing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_failed(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
PayoutStatus::Pending => false,
|
||||||
|
PayoutStatus::Failed => true,
|
||||||
|
PayoutStatus::Processed => false,
|
||||||
|
PayoutStatus::Returned => true,
|
||||||
|
PayoutStatus::Processing => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -14,6 +14,12 @@ pub struct AnalyticsQueue {
|
|||||||
playtime_queue: DashSet<Playtime>,
|
playtime_queue: DashSet<Playtime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for AnalyticsQueue {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Batches analytics data points + transactions every few minutes
|
// Batches analytics data points + transactions every few minutes
|
||||||
impl AnalyticsQueue {
|
impl AnalyticsQueue {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
|||||||
@ -6,6 +6,12 @@ pub struct DownloadQueue {
|
|||||||
queue: Mutex<Vec<(ProjectId, VersionId)>>,
|
queue: Mutex<Vec<(ProjectId, VersionId)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for DownloadQueue {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Batches download transactions every thirty seconds
|
// Batches download transactions every thirty seconds
|
||||||
impl DownloadQueue {
|
impl DownloadQueue {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
|||||||
@ -1,203 +1,332 @@
|
|||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use crate::util::env::parse_var;
|
use crate::util::env::parse_var;
|
||||||
use crate::{database::redis::RedisPool, models::projects::MonetizationStatus};
|
use crate::{database::redis::RedisPool, models::projects::MonetizationStatus};
|
||||||
use base64::Engine;
|
|
||||||
use chrono::{DateTime, Datelike, Duration, Utc, Weekday};
|
use chrono::{DateTime, Datelike, Duration, Utc, Weekday};
|
||||||
|
use hex::ToHex;
|
||||||
|
use hmac::{Hmac, Mac, NewMac};
|
||||||
|
use reqwest::Method;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::{json, Value};
|
||||||
|
use sha2::Sha256;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
pub struct PayoutsQueue {
|
pub struct PayoutsQueue {
|
||||||
credential: PaypalCredential,
|
access_key: String,
|
||||||
credential_expires: DateTime<Utc>,
|
secret_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
impl Default for PayoutsQueue {
|
||||||
struct PaypalCredential {
|
fn default() -> Self {
|
||||||
access_token: String,
|
Self::new()
|
||||||
token_type: String,
|
}
|
||||||
expires_in: i64,
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum AccountUser {
|
||||||
|
Business { name: String },
|
||||||
|
Individual { first: String, last: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct PayoutItem {
|
pub struct PaymentInfo {
|
||||||
pub amount: PayoutAmount,
|
country: String,
|
||||||
pub receiver: String,
|
payout_method: String,
|
||||||
pub note: String,
|
route_minimum: Decimal,
|
||||||
pub recipient_type: String,
|
estimated_fees: Decimal,
|
||||||
pub recipient_wallet: String,
|
deduct_fees: Decimal,
|
||||||
pub sender_item_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct PayoutAmount {
|
|
||||||
pub currency: String,
|
|
||||||
#[serde(with = "rust_decimal::serde::str")]
|
|
||||||
pub value: Decimal,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batches payouts and handles token refresh
|
// Batches payouts and handles token refresh
|
||||||
impl PayoutsQueue {
|
impl PayoutsQueue {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
PayoutsQueue {
|
PayoutsQueue {
|
||||||
credential: Default::default(),
|
access_key: dotenvy::var("TROLLEY_ACCESS_KEY").expect("missing trolley access key"),
|
||||||
credential_expires: Utc::now() - Duration::days(30),
|
secret_key: dotenvy::var("TROLLEY_SECRET_KEY").expect("missing trolley secret key"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh_token(&mut self) -> Result<(), ApiError> {
|
pub async fn make_trolley_request<T: Serialize, X: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
method: Method,
|
||||||
|
path: &str,
|
||||||
|
body: Option<T>,
|
||||||
|
) -> Result<X, ApiError> {
|
||||||
|
let timestamp = Utc::now().timestamp();
|
||||||
|
|
||||||
|
let mut mac: Hmac<Sha256> = Hmac::new_from_slice(self.secret_key.as_bytes())
|
||||||
|
.map_err(|_| ApiError::Payments("error initializing HMAC".to_string()))?;
|
||||||
|
mac.update(
|
||||||
|
if let Some(body) = &body {
|
||||||
|
format!(
|
||||||
|
"{}\n{}\n{}\n{}\n",
|
||||||
|
timestamp,
|
||||||
|
method.as_str(),
|
||||||
|
path,
|
||||||
|
serde_json::to_string(&body)?
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{}\n{}\n{}\n\n", timestamp, method.as_str(), path)
|
||||||
|
}
|
||||||
|
.as_bytes(),
|
||||||
|
);
|
||||||
|
let request_signature = mac.finalize().into_bytes().encode_hex::<String>();
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
let combined_key = format!(
|
let mut request = client
|
||||||
"{}:{}",
|
.request(method, format!("https://api.trolley.com{path}"))
|
||||||
dotenvy::var("PAYPAL_CLIENT_ID")?,
|
.header(
|
||||||
dotenvy::var("PAYPAL_CLIENT_SECRET")?
|
"Authorization",
|
||||||
);
|
format!("prsign {}:{}", self.access_key, request_signature),
|
||||||
let formatted_key = format!(
|
)
|
||||||
"Basic {}",
|
.header("X-PR-Timestamp", timestamp);
|
||||||
base64::engine::general_purpose::STANDARD.encode(combined_key)
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut form = HashMap::new();
|
if let Some(body) = body {
|
||||||
form.insert("grant_type", "client_credentials");
|
request = request.json(&body);
|
||||||
|
}
|
||||||
|
|
||||||
let credential: PaypalCredential = client
|
let resp = request
|
||||||
.post(&format!("{}oauth2/token", dotenvy::var("PAYPAL_API_URL")?))
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.header("Accept-Language", "en_US")
|
|
||||||
.header("Authorization", formatted_key)
|
|
||||||
.form(&form)
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ApiError::Payments("Error while authenticating with PayPal".to_string()))?
|
.map_err(|_| ApiError::Payments("could not communicate with Trolley".to_string()))?;
|
||||||
.json()
|
|
||||||
.await
|
let value = resp.json::<Value>().await.map_err(|_| {
|
||||||
.map_err(|_| {
|
ApiError::Payments("could not retrieve Trolley response body".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Some(obj) = value.as_object() {
|
||||||
|
if !obj.get("ok").and_then(|x| x.as_bool()).unwrap_or(true) {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TrolleyError {
|
||||||
|
field: Option<String>,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(array) = obj.get("errors") {
|
||||||
|
let err = serde_json::from_value::<Vec<TrolleyError>>(array.clone()).map_err(
|
||||||
|
|_| {
|
||||||
ApiError::Payments(
|
ApiError::Payments(
|
||||||
"Error while authenticating with PayPal (deser error)".to_string(),
|
"could not retrieve Trolley error json body".to_string(),
|
||||||
)
|
)
|
||||||
})?;
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
self.credential_expires = Utc::now() + Duration::seconds(credential.expires_in);
|
if let Some(first) = err.into_iter().next() {
|
||||||
self.credential = credential;
|
return Err(ApiError::Payments(if let Some(field) = &first.field {
|
||||||
|
format!("error - field: {field} message: {}", first.message)
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_payout(&mut self, mut payout: PayoutItem) -> Result<Decimal, ApiError> {
|
|
||||||
if self.credential_expires < Utc::now() {
|
|
||||||
self.refresh_token().await.map_err(|_| {
|
|
||||||
ApiError::Payments("Error while authenticating with PayPal".to_string())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let wallet = payout.recipient_wallet.clone();
|
|
||||||
|
|
||||||
let fee = if wallet == *"Venmo" {
|
|
||||||
Decimal::ONE / Decimal::from(4)
|
|
||||||
} else {
|
} else {
|
||||||
|
first.message
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(ApiError::Payments(
|
||||||
|
"could not retrieve Trolley error body".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_json::from_value(value)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_payout(
|
||||||
|
&mut self,
|
||||||
|
recipient: &str,
|
||||||
|
amount: Decimal,
|
||||||
|
) -> Result<(String, Option<String>), ApiError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TrolleyRes {
|
||||||
|
batch: Batch,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Batch {
|
||||||
|
id: String,
|
||||||
|
payments: BatchPayments,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Payment {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct BatchPayments {
|
||||||
|
payments: Vec<Payment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let fee = self.get_estimated_fees(recipient, amount).await?;
|
||||||
|
|
||||||
|
if fee.estimated_fees > amount || fee.route_minimum > amount {
|
||||||
|
return Err(ApiError::Payments(
|
||||||
|
"Account balance is too low to withdraw funds".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let send_amount = amount - fee.deduct_fees;
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.make_trolley_request::<_, TrolleyRes>(
|
||||||
|
Method::POST,
|
||||||
|
"/v1/batches/",
|
||||||
|
Some(json!({
|
||||||
|
"currency": "USD",
|
||||||
|
"description": "labrinth payout",
|
||||||
|
"payments": [{
|
||||||
|
"recipient": {
|
||||||
|
"id": recipient
|
||||||
|
},
|
||||||
|
"amount": send_amount.to_string(),
|
||||||
|
"currency": "USD",
|
||||||
|
"memo": "Modrinth ad revenue payout"
|
||||||
|
}],
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.make_trolley_request::<Value, Value>(
|
||||||
|
Method::POST,
|
||||||
|
&format!("/v1/batches/{}/start-processing", res.batch.id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payment_id = res.batch.payments.payments.into_iter().next().map(|x| x.id);
|
||||||
|
|
||||||
|
Ok((res.batch.id, payment_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register_recipient(
|
||||||
|
&self,
|
||||||
|
email: &str,
|
||||||
|
user: AccountUser,
|
||||||
|
) -> Result<String, ApiError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TrolleyRes {
|
||||||
|
recipient: Recipient,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Recipient {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = self
|
||||||
|
.make_trolley_request::<_, TrolleyRes>(
|
||||||
|
Method::POST,
|
||||||
|
"/v1/recipients/",
|
||||||
|
Some(match user {
|
||||||
|
AccountUser::Business { name } => json!({
|
||||||
|
"type": "business",
|
||||||
|
"email": email,
|
||||||
|
"name": name,
|
||||||
|
}),
|
||||||
|
AccountUser::Individual { first, last } => json!({
|
||||||
|
"type": "individual",
|
||||||
|
"firstName": first,
|
||||||
|
"lastName": last,
|
||||||
|
"email": email,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(id.recipient.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lhs minimum, rhs estimate
|
||||||
|
pub async fn get_estimated_fees(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
amount: Decimal,
|
||||||
|
) -> Result<PaymentInfo, ApiError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TrolleyRes {
|
||||||
|
recipient: Recipient,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Recipient {
|
||||||
|
route_minimum: Option<Decimal>,
|
||||||
|
estimated_fees: Option<Decimal>,
|
||||||
|
address: RecipientAddress,
|
||||||
|
payout_method: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RecipientAddress {
|
||||||
|
country: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = self
|
||||||
|
.make_trolley_request::<Value, TrolleyRes>(
|
||||||
|
Method::GET,
|
||||||
|
&format!("/v1/recipients/{id}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if &id.recipient.payout_method == "paypal" {
|
||||||
|
// based on https://www.paypal.com/us/webapps/mpp/merchant-fees. see paypal payouts section
|
||||||
|
let fee = if &id.recipient.address.country == "US" {
|
||||||
std::cmp::min(
|
std::cmp::min(
|
||||||
std::cmp::max(
|
std::cmp::max(
|
||||||
Decimal::ONE / Decimal::from(4),
|
Decimal::ONE / Decimal::from(4),
|
||||||
(Decimal::from(2) / Decimal::ONE_HUNDRED) * payout.amount.value,
|
(Decimal::from(2) / Decimal::ONE_HUNDRED) * amount,
|
||||||
),
|
),
|
||||||
|
Decimal::from(1),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
std::cmp::min(
|
||||||
|
(Decimal::from(2) / Decimal::ONE_HUNDRED) * amount,
|
||||||
Decimal::from(20),
|
Decimal::from(20),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
payout.amount.value -= fee;
|
Ok(PaymentInfo {
|
||||||
payout.amount.value = payout.amount.value.round_dp(2);
|
country: id.recipient.address.country,
|
||||||
|
payout_method: id.recipient.payout_method,
|
||||||
|
route_minimum: fee,
|
||||||
|
estimated_fees: fee,
|
||||||
|
deduct_fees: fee,
|
||||||
|
})
|
||||||
|
} else if &id.recipient.payout_method == "venmo" {
|
||||||
|
let venmo_fee = Decimal::ONE / Decimal::from(4);
|
||||||
|
|
||||||
if payout.amount.value <= Decimal::ZERO {
|
Ok(PaymentInfo {
|
||||||
return Err(ApiError::InvalidInput(
|
country: id.recipient.address.country,
|
||||||
"You do not have enough funds to make this payout!".to_string(),
|
payout_method: id.recipient.payout_method,
|
||||||
));
|
route_minimum: id.recipient.route_minimum.unwrap_or(Decimal::ZERO) + venmo_fee,
|
||||||
|
estimated_fees: id.recipient.estimated_fees.unwrap_or(Decimal::ZERO) + venmo_fee,
|
||||||
|
deduct_fees: venmo_fee,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(PaymentInfo {
|
||||||
|
country: id.recipient.address.country,
|
||||||
|
payout_method: id.recipient.payout_method,
|
||||||
|
route_minimum: id.recipient.route_minimum.unwrap_or(Decimal::ZERO),
|
||||||
|
estimated_fees: id.recipient.estimated_fees.unwrap_or(Decimal::ZERO),
|
||||||
|
deduct_fees: Decimal::ZERO,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
pub async fn update_recipient_email(&self, id: &str, email: &str) -> Result<(), ApiError> {
|
||||||
|
self.make_trolley_request::<_, Value>(
|
||||||
let res = client.post(&format!("{}payments/payouts", dotenvy::var("PAYPAL_API_URL")?))
|
Method::PATCH,
|
||||||
.header("Authorization", format!("{} {}", self.credential.token_type, self.credential.access_token))
|
&format!("/v1/recipients/{}", id),
|
||||||
.json(&json! ({
|
Some(json!({
|
||||||
"sender_batch_header": {
|
"email": email,
|
||||||
"sender_batch_id": format!("{}-payouts", Utc::now().to_rfc3339()),
|
})),
|
||||||
"email_subject": "You have received a payment from Modrinth!",
|
|
||||||
"email_message": "Thank you for creating projects on Modrinth. Please claim this payment within 30 days.",
|
|
||||||
},
|
|
||||||
"items": vec![payout]
|
|
||||||
}))
|
|
||||||
.send().await.map_err(|_| ApiError::Payments("Error while sending payout to PayPal".to_string()))?;
|
|
||||||
|
|
||||||
if !res.status().is_success() {
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PayPalError {
|
|
||||||
pub body: PayPalErrorBody,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PayPalErrorBody {
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let body: PayPalError = res.json().await.map_err(|_| {
|
|
||||||
ApiError::Payments("Error while registering payment in PayPal!".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
return Err(ApiError::Payments(format!(
|
|
||||||
"Error while registering payment in PayPal: {}",
|
|
||||||
body.body.message
|
|
||||||
)));
|
|
||||||
} else if wallet != *"Venmo" {
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PayPalLink {
|
|
||||||
href: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PayoutsResponse {
|
|
||||||
pub links: Vec<PayPalLink>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PayoutDataItem {
|
|
||||||
payout_item_fee: PayoutAmount,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PayoutData {
|
|
||||||
pub items: Vec<PayoutDataItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate actual fee + refund if we took too big of a fee.
|
|
||||||
if let Ok(res) = res.json::<PayoutsResponse>().await {
|
|
||||||
if let Some(link) = res.links.first() {
|
|
||||||
if let Ok(res) = client
|
|
||||||
.get(&link.href)
|
|
||||||
.header(
|
|
||||||
"Authorization",
|
|
||||||
format!(
|
|
||||||
"{} {}",
|
|
||||||
self.credential.token_type, self.credential.access_token
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.send()
|
.await?;
|
||||||
.await
|
|
||||||
{
|
|
||||||
if let Ok(res) = res.json::<PayoutData>().await {
|
|
||||||
if let Some(data) = res.items.first() {
|
|
||||||
if (fee - data.payout_item_fee.value) > Decimal::ZERO {
|
|
||||||
return Ok(fee - data.payout_item_fee.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Decimal::ZERO)
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,7 +335,7 @@ pub async fn process_payout(
|
|||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
client: &clickhouse::Client,
|
client: &clickhouse::Client,
|
||||||
) -> Result<(), ApiError> {
|
) -> Result<(), ApiError> {
|
||||||
let start: DateTime<Utc> = DateTime::from_utc(
|
let start: DateTime<Utc> = DateTime::from_naive_utc_and_offset(
|
||||||
(Utc::now() - Duration::days(1))
|
(Utc::now() - Duration::days(1))
|
||||||
.date_naive()
|
.date_naive()
|
||||||
.and_hms_nano_opt(0, 0, 0, 0)
|
.and_hms_nano_opt(0, 0, 0, 0)
|
||||||
|
|||||||
@ -13,6 +13,12 @@ pub struct AuthQueue {
|
|||||||
pat_queue: Mutex<HashSet<PatId>>,
|
pat_queue: Mutex<HashSet<PatId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for AuthQueue {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Batches session accessing transactions every 30 seconds
|
// Batches session accessing transactions every 30 seconds
|
||||||
impl AuthQueue {
|
impl AuthQueue {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
|||||||
@ -15,6 +15,12 @@ pub struct MemoryStore {
|
|||||||
inner: Arc<DashMap<String, (usize, Duration)>>,
|
inner: Arc<DashMap<String, (usize, Duration)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for MemoryStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MemoryStore {
|
impl MemoryStore {
|
||||||
/// Create a new hashmap
|
/// Create a new hashmap
|
||||||
///
|
///
|
||||||
|
|||||||
@ -6,10 +6,10 @@ use crate::queue::analytics::AnalyticsQueue;
|
|||||||
use crate::queue::maxmind::MaxMindIndexer;
|
use crate::queue::maxmind::MaxMindIndexer;
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
|
use crate::util::date::get_current_tenths_of_ms;
|
||||||
use crate::util::env::parse_strings_from_var;
|
use crate::util::env::parse_strings_from_var;
|
||||||
use actix_web::{post, web};
|
use actix_web::{post, web};
|
||||||
use actix_web::{HttpRequest, HttpResponse};
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
use chrono::Utc;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -108,7 +108,7 @@ pub async fn page_view_ingest(
|
|||||||
|
|
||||||
let mut view = PageView {
|
let mut view = PageView {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
recorded: Utc::now().timestamp_nanos() / 100_000,
|
recorded: get_current_tenths_of_ms(),
|
||||||
domain: domain.to_string(),
|
domain: domain.to_string(),
|
||||||
site_path: url.path().to_string(),
|
site_path: url.path().to_string(),
|
||||||
user_id: 0,
|
user_id: 0,
|
||||||
@ -204,7 +204,7 @@ pub async fn playtime_ingest(
|
|||||||
if let Some(version) = versions.iter().find(|x| id == x.inner.id.into()) {
|
if let Some(version) = versions.iter().find(|x| id == x.inner.id.into()) {
|
||||||
analytics_queue.add_playtime(Playtime {
|
analytics_queue.add_playtime(Playtime {
|
||||||
id: Default::default(),
|
id: Default::default(),
|
||||||
recorded: Utc::now().timestamp_nanos() / 100_000,
|
recorded: get_current_tenths_of_ms(),
|
||||||
seconds: playtime.seconds as u64,
|
seconds: playtime.seconds as u64,
|
||||||
user_id: user.id.0,
|
user_id: user.id.0,
|
||||||
project_id: version.inner.project_id.0 as u64,
|
project_id: version.inner.project_id.0 as u64,
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
use crate::auth::validate::get_user_record_from_bearer_token;
|
use crate::auth::validate::get_user_record_from_bearer_token;
|
||||||
|
use crate::database::models::User;
|
||||||
use crate::database::redis::RedisPool;
|
use crate::database::redis::RedisPool;
|
||||||
use crate::models::analytics::Download;
|
use crate::models::analytics::Download;
|
||||||
use crate::models::ids::ProjectId;
|
use crate::models::ids::ProjectId;
|
||||||
use crate::models::pats::Scopes;
|
use crate::models::pats::Scopes;
|
||||||
|
use crate::models::users::{PayoutStatus, RecipientStatus};
|
||||||
use crate::queue::analytics::AnalyticsQueue;
|
use crate::queue::analytics::AnalyticsQueue;
|
||||||
use crate::queue::download::DownloadQueue;
|
use crate::queue::download::DownloadQueue;
|
||||||
use crate::queue::maxmind::MaxMindIndexer;
|
use crate::queue::maxmind::MaxMindIndexer;
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
|
use crate::util::date::get_current_tenths_of_ms;
|
||||||
use crate::util::guards::admin_key_guard;
|
use crate::util::guards::admin_key_guard;
|
||||||
use actix_web::{patch, web, HttpRequest, HttpResponse};
|
use crate::util::routes::read_from_payload;
|
||||||
use chrono::Utc;
|
use actix_web::{patch, post, web, HttpRequest, HttpResponse};
|
||||||
|
use hex::ToHex;
|
||||||
|
use hmac::{Hmac, Mac, NewMac};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use sha2::Sha256;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
@ -19,7 +25,11 @@ use std::sync::Arc;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(web::scope("admin").service(count_download));
|
cfg.service(
|
||||||
|
web::scope("admin")
|
||||||
|
.service(count_download)
|
||||||
|
.service(trolley_webhook),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -110,7 +120,7 @@ pub async fn count_download(
|
|||||||
|
|
||||||
analytics_queue.add_download(Download {
|
analytics_queue.add_download(Download {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
recorded: Utc::now().timestamp_nanos() / 100_000,
|
recorded: get_current_tenths_of_ms(),
|
||||||
domain: url.host_str().unwrap_or_default().to_string(),
|
domain: url.host_str().unwrap_or_default().to_string(),
|
||||||
site_path: url.path().to_string(),
|
site_path: url.path().to_string(),
|
||||||
user_id: user
|
user_id: user
|
||||||
@ -141,3 +151,171 @@ pub async fn count_download(
|
|||||||
|
|
||||||
Ok(HttpResponse::NoContent().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct TrolleyWebhook {
|
||||||
|
model: String,
|
||||||
|
action: String,
|
||||||
|
body: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/_trolley")]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub async fn trolley_webhook(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
mut payload: web::Payload,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
if let Some(signature) = req.headers().get("X-PaymentRails-Signature") {
|
||||||
|
let payload = read_from_payload(
|
||||||
|
&mut payload,
|
||||||
|
1 << 20,
|
||||||
|
"Webhook payload exceeds the maximum of 1MiB.",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut signature = signature.to_str().ok().unwrap_or_default().split(',');
|
||||||
|
let timestamp = signature
|
||||||
|
.next()
|
||||||
|
.and_then(|x| x.split('=').nth(1))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let v1 = signature
|
||||||
|
.next()
|
||||||
|
.and_then(|x| x.split('=').nth(1))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut mac: Hmac<Sha256> =
|
||||||
|
Hmac::new_from_slice(dotenvy::var("TROLLEY_WEBHOOK_SIGNATURE")?.as_bytes())
|
||||||
|
.map_err(|_| ApiError::Payments("error initializing HMAC".to_string()))?;
|
||||||
|
mac.update(timestamp.as_bytes());
|
||||||
|
mac.update(&payload);
|
||||||
|
let request_signature = mac.finalize().into_bytes().encode_hex::<String>();
|
||||||
|
|
||||||
|
if &*request_signature == v1 {
|
||||||
|
let webhook = serde_json::from_slice::<TrolleyWebhook>(&payload)?;
|
||||||
|
|
||||||
|
if webhook.model == "recipient" {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Recipient {
|
||||||
|
pub id: String,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub status: Option<RecipientStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(body) = webhook.body.get("recipient") {
|
||||||
|
if let Ok(recipient) = serde_json::from_value::<Recipient>(body.clone()) {
|
||||||
|
let value = sqlx::query!(
|
||||||
|
"SELECT id FROM users WHERE trolley_id = $1",
|
||||||
|
recipient.id
|
||||||
|
)
|
||||||
|
.fetch_optional(&**pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(user) = value {
|
||||||
|
let user = User::get_id(
|
||||||
|
crate::database::models::UserId(user.id),
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(user) = user {
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
|
if webhook.action == "deleted" {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE users
|
||||||
|
SET trolley_account_status = NULL, trolley_id = NULL
|
||||||
|
WHERE id = $1
|
||||||
|
",
|
||||||
|
user.id.0
|
||||||
|
)
|
||||||
|
.execute(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE users
|
||||||
|
SET email = $1, email_verified = $2, trolley_account_status = $3
|
||||||
|
WHERE id = $4
|
||||||
|
",
|
||||||
|
recipient.email.clone(),
|
||||||
|
user.email_verified && recipient.email == user.email,
|
||||||
|
recipient.status.map(|x| x.as_str()),
|
||||||
|
user.id.0
|
||||||
|
)
|
||||||
|
.execute(&mut transaction).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
User::clear_caches(&[(user.id, None)], &redis).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if webhook.model == "payment" {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Payment {
|
||||||
|
pub id: String,
|
||||||
|
pub status: PayoutStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(body) = webhook.body.get("payment") {
|
||||||
|
if let Ok(payment) = serde_json::from_value::<Payment>(body.clone()) {
|
||||||
|
let value = sqlx::query!(
|
||||||
|
"SELECT id, amount, user_id, status FROM historical_payouts WHERE payment_id = $1",
|
||||||
|
payment.id
|
||||||
|
)
|
||||||
|
.fetch_optional(&**pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(payout) = value {
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
|
if payment.status.is_failed()
|
||||||
|
&& !PayoutStatus::from_string(&payout.status).is_failed()
|
||||||
|
{
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE users
|
||||||
|
SET balance = balance + $1
|
||||||
|
WHERE id = $2
|
||||||
|
",
|
||||||
|
payout.amount,
|
||||||
|
payout.user_id,
|
||||||
|
)
|
||||||
|
.execute(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE historical_payouts
|
||||||
|
SET status = $1
|
||||||
|
WHERE payment_id = $2
|
||||||
|
",
|
||||||
|
payment.status.as_str(),
|
||||||
|
payment.id,
|
||||||
|
)
|
||||||
|
.execute(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
User::clear_caches(
|
||||||
|
&[(crate::database::models::UserId(payout.user_id), None)],
|
||||||
|
&redis,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::NoContent().finish())
|
||||||
|
}
|
||||||
|
|||||||
@ -32,7 +32,6 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
|||||||
.configure(moderation::config)
|
.configure(moderation::config)
|
||||||
.configure(notifications::config)
|
.configure(notifications::config)
|
||||||
.configure(organizations::config)
|
.configure(organizations::config)
|
||||||
//.configure(pats::config)
|
|
||||||
.configure(project_creation::config)
|
.configure(project_creation::config)
|
||||||
.configure(collections::config)
|
.configure(collections::config)
|
||||||
.configure(images::config)
|
.configure(images::config)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::auth::{get_user_from_headers, AuthenticationError};
|
use crate::auth::get_user_from_headers;
|
||||||
use crate::database::models::User;
|
use crate::database::models::User;
|
||||||
use crate::database::redis::RedisPool;
|
use crate::database::redis::RedisPool;
|
||||||
use crate::file_hosting::FileHost;
|
use crate::file_hosting::FileHost;
|
||||||
@ -6,14 +6,15 @@ use crate::models::collections::{Collection, CollectionStatus};
|
|||||||
use crate::models::notifications::Notification;
|
use crate::models::notifications::Notification;
|
||||||
use crate::models::pats::Scopes;
|
use crate::models::pats::Scopes;
|
||||||
use crate::models::projects::Project;
|
use crate::models::projects::Project;
|
||||||
use crate::models::users::{Badges, RecipientType, RecipientWallet, Role, UserId};
|
use crate::models::users::{
|
||||||
use crate::queue::payouts::{PayoutAmount, PayoutItem, PayoutsQueue};
|
Badges, Payout, PayoutStatus, RecipientStatus, Role, UserId, UserPayoutData,
|
||||||
|
};
|
||||||
|
use crate::queue::payouts::PayoutsQueue;
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use crate::util::routes::read_from_payload;
|
use crate::util::routes::read_from_payload;
|
||||||
use crate::util::validate::validation_errors_to_string;
|
use crate::util::validate::validation_errors_to_string;
|
||||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
@ -39,6 +40,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
.service(user_notifications)
|
.service(user_notifications)
|
||||||
.service(user_follows)
|
.service(user_follows)
|
||||||
.service(user_payouts)
|
.service(user_payouts)
|
||||||
|
.service(user_payouts_fees)
|
||||||
.service(user_payouts_request),
|
.service(user_payouts_request),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -218,21 +220,6 @@ pub struct EditUser {
|
|||||||
pub bio: Option<Option<String>>,
|
pub bio: Option<Option<String>>,
|
||||||
pub role: Option<Role>,
|
pub role: Option<Role>,
|
||||||
pub badges: Option<Badges>,
|
pub badges: Option<Badges>,
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
skip_serializing_if = "Option::is_none",
|
|
||||||
with = "::serde_with::rust::double_option"
|
|
||||||
)]
|
|
||||||
#[validate]
|
|
||||||
pub payout_data: Option<Option<EditPayoutData>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Validate)]
|
|
||||||
pub struct EditPayoutData {
|
|
||||||
pub payout_wallet: RecipientWallet,
|
|
||||||
pub payout_wallet_type: RecipientType,
|
|
||||||
#[validate(length(max = 128))]
|
|
||||||
pub payout_address: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[patch("{id}")]
|
#[patch("{id}")]
|
||||||
@ -244,7 +231,7 @@ pub async fn user_edit(
|
|||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
session_queue: web::Data<AuthQueue>,
|
session_queue: web::Data<AuthQueue>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let (scopes, user) = get_user_from_headers(
|
let (_scopes, user) = get_user_from_headers(
|
||||||
&req,
|
&req,
|
||||||
&**pool,
|
&**pool,
|
||||||
&redis,
|
&redis,
|
||||||
@ -364,79 +351,6 @@ pub async fn user_edit(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(payout_data) = &new_user.payout_data {
|
|
||||||
if let Some(payout_data) = payout_data {
|
|
||||||
if payout_data.payout_wallet_type == RecipientType::UserHandle
|
|
||||||
&& payout_data.payout_wallet == RecipientWallet::Paypal
|
|
||||||
{
|
|
||||||
return Err(ApiError::InvalidInput(
|
|
||||||
"You cannot use a paypal wallet with a user handle!".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
RecipientType::Phone => {
|
|
||||||
validator::validate_phone(&payout_data.payout_address)
|
|
||||||
}
|
|
||||||
RecipientType::UserHandle => true,
|
|
||||||
} {
|
|
||||||
return Err(ApiError::InvalidInput(
|
|
||||||
"Invalid wallet specified!".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let results = sqlx::query!(
|
|
||||||
"
|
|
||||||
SELECT EXISTS(SELECT 1 FROM users WHERE id = $1 AND email IS NULL)
|
|
||||||
",
|
|
||||||
id as crate::database::models::ids::UserId,
|
|
||||||
)
|
|
||||||
.fetch_one(&mut *transaction)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if results.exists.unwrap_or(false) {
|
|
||||||
return Err(ApiError::InvalidInput(
|
|
||||||
"You must have an email set on your Modrinth account to enroll in the monetization program!"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
"
|
|
||||||
UPDATE users
|
|
||||||
SET payout_wallet = $1, payout_wallet_type = $2, payout_address = $3
|
|
||||||
WHERE (id = $4)
|
|
||||||
",
|
|
||||||
payout_data.payout_wallet.as_str(),
|
|
||||||
payout_data.payout_wallet_type.as_str(),
|
|
||||||
payout_data.payout_address,
|
|
||||||
id as crate::database::models::ids::UserId,
|
|
||||||
)
|
|
||||||
.execute(&mut *transaction)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
sqlx::query!(
|
|
||||||
"
|
|
||||||
UPDATE users
|
|
||||||
SET payout_wallet = NULL, payout_wallet_type = NULL, payout_address = NULL
|
|
||||||
WHERE (id = $1)
|
|
||||||
",
|
|
||||||
id as crate::database::models::ids::UserId,
|
|
||||||
)
|
|
||||||
.execute(&mut *transaction)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?;
|
User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?;
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
Ok(HttpResponse::NoContent().body(""))
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
@ -691,13 +605,6 @@ pub async fn user_notifications(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Payout {
|
|
||||||
pub created: DateTime<Utc>,
|
|
||||||
pub amount: Decimal,
|
|
||||||
pub status: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("{id}/payouts")]
|
#[get("{id}/payouts")]
|
||||||
pub async fn user_payouts(
|
pub async fn user_payouts(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
@ -757,7 +664,7 @@ pub async fn user_payouts(
|
|||||||
Ok(e.right().map(|row| Payout {
|
Ok(e.right().map(|row| Payout {
|
||||||
created: row.created,
|
created: row.created,
|
||||||
amount: row.amount,
|
amount: row.amount,
|
||||||
status: row.status,
|
status: PayoutStatus::from_string(&row.status),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.try_collect::<Vec<Payout>>(),
|
.try_collect::<Vec<Payout>>(),
|
||||||
@ -776,6 +683,61 @@ pub async fn user_payouts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct FeeEstimateAmount {
|
||||||
|
amount: Decimal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("{id}/payouts_fees")]
|
||||||
|
pub async fn user_payouts_fees(
|
||||||
|
req: HttpRequest,
|
||||||
|
info: web::Path<(String,)>,
|
||||||
|
web::Query(amount): web::Query<FeeEstimateAmount>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
payouts_queue: web::Data<Mutex<PayoutsQueue>>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let user = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Some(&[Scopes::PAYOUTS_READ]),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.1;
|
||||||
|
let actual_user = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||||
|
|
||||||
|
if let Some(actual_user) = actual_user {
|
||||||
|
if !user.role.is_admin() && user.id != actual_user.id.into() {
|
||||||
|
return Err(ApiError::CustomAuthentication(
|
||||||
|
"You do not have permission to request payouts of this user!".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(UserPayoutData {
|
||||||
|
trolley_id: Some(trolley_id),
|
||||||
|
..
|
||||||
|
}) = user.payout_data
|
||||||
|
{
|
||||||
|
let payouts = payouts_queue
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get_estimated_fees(&trolley_id, amount.amount)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(payouts))
|
||||||
|
} else {
|
||||||
|
Err(ApiError::InvalidInput(
|
||||||
|
"You must set up your trolley account first!".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().body(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct PayoutData {
|
pub struct PayoutData {
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
@ -811,40 +773,30 @@ pub async fn user_payouts_request(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(payouts_data) = user.payout_data {
|
if let Some(UserPayoutData {
|
||||||
if let Some(payout_address) = payouts_data.payout_address {
|
trolley_id: Some(trolley_id),
|
||||||
if let Some(payout_wallet_type) = payouts_data.payout_wallet_type {
|
trolley_status: Some(trolley_status),
|
||||||
if let Some(payout_wallet) = payouts_data.payout_wallet {
|
balance,
|
||||||
return if data.amount < payouts_data.balance {
|
..
|
||||||
|
}) = user.payout_data
|
||||||
|
{
|
||||||
|
if trolley_status == RecipientStatus::Active {
|
||||||
|
return if data.amount < balance {
|
||||||
let mut transaction = pool.begin().await?;
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
let leftover = payouts_queue
|
let (batch_id, payment_id) =
|
||||||
.send_payout(PayoutItem {
|
payouts_queue.send_payout(&trolley_id, data.amount).await?;
|
||||||
amount: PayoutAmount {
|
|
||||||
currency: "USD".to_string(),
|
|
||||||
value: data.amount,
|
|
||||||
},
|
|
||||||
receiver: payout_address,
|
|
||||||
note: "Payment from Modrinth creator monetization program"
|
|
||||||
.to_string(),
|
|
||||||
recipient_type: payout_wallet_type.to_string().to_uppercase(),
|
|
||||||
recipient_wallet: payout_wallet.as_str_api().to_string(),
|
|
||||||
sender_item_id: format!(
|
|
||||||
"{}-{}",
|
|
||||||
UserId::from(id),
|
|
||||||
Utc::now().timestamp()
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO historical_payouts (user_id, amount, status)
|
INSERT INTO historical_payouts (user_id, amount, status, batch_id, payment_id)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
",
|
",
|
||||||
id as crate::database::models::ids::UserId,
|
id as crate::database::models::ids::UserId,
|
||||||
data.amount,
|
data.amount,
|
||||||
"success"
|
"processing",
|
||||||
|
batch_id,
|
||||||
|
payment_id,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@ -855,11 +807,12 @@ pub async fn user_payouts_request(
|
|||||||
SET balance = balance - $1
|
SET balance = balance - $1
|
||||||
WHERE id = $2
|
WHERE id = $2
|
||||||
",
|
",
|
||||||
data.amount - leftover,
|
data.amount,
|
||||||
id as crate::database::models::ids::UserId
|
id as crate::database::models::ids::UserId
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
User::clear_caches(&[(id, None)], &redis).await?;
|
User::clear_caches(&[(id, None)], &redis).await?;
|
||||||
|
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
@ -870,8 +823,10 @@ pub async fn user_payouts_request(
|
|||||||
"You do not have enough funds to make this payout!".to_string(),
|
"You do not have enough funds to make this payout!".to_string(),
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
}
|
} else {
|
||||||
}
|
return Err(ApiError::InvalidInput(
|
||||||
|
"Please complete payout information via the trolley dashboard!".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,12 @@ pub struct Scheduler {
|
|||||||
arbiter: Arbiter,
|
arbiter: Arbiter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Scheduler {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Scheduler {
|
impl Scheduler {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Scheduler {
|
Scheduler {
|
||||||
|
|||||||
18
src/util/bitflag.rs
Normal file
18
src/util/bitflag.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! bitflags_serde_impl {
|
||||||
|
($type:ident, $int_type:ident) => {
|
||||||
|
impl serde::Serialize for $type {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
serializer.serialize_i64(self.bits() as i64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for $type {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
let v: i64 = Deserialize::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
Ok($type::from_bits_truncate(v as $int_type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
9
src/util/date.rs
Normal file
9
src/util/date.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
// this converts timestamps to the timestamp format clickhouse requires/uses
|
||||||
|
pub fn get_current_tenths_of_ms() -> i64 {
|
||||||
|
Utc::now()
|
||||||
|
.timestamp_nanos_opt()
|
||||||
|
.expect("value can not be represented in a timestamp with nanosecond precision.")
|
||||||
|
/ 100_000
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
|
pub mod bitflag;
|
||||||
pub mod captcha;
|
pub mod captcha;
|
||||||
pub mod cors;
|
pub mod cors;
|
||||||
|
pub mod date;
|
||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod ext;
|
pub mod ext;
|
||||||
pub mod guards;
|
pub mod guards;
|
||||||
|
|||||||
@ -20,7 +20,7 @@ impl super::Validator for FabricValidator {
|
|||||||
|
|
||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// Time since release of 18w49a, the first fabric version
|
// Time since release of 18w49a, the first fabric version
|
||||||
SupportedGameVersions::PastDate(DateTime::from_utc(
|
SupportedGameVersions::PastDate(DateTime::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::from_timestamp_opt(1543969469, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(1543969469, 0).unwrap(),
|
||||||
Utc,
|
Utc,
|
||||||
))
|
))
|
||||||
|
|||||||
@ -20,7 +20,7 @@ impl super::Validator for ForgeValidator {
|
|||||||
|
|
||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// Time since release of 1.13, the first forge version which uses the new TOML system
|
// Time since release of 1.13, the first forge version which uses the new TOML system
|
||||||
SupportedGameVersions::PastDate(DateTime::<Utc>::from_utc(
|
SupportedGameVersions::PastDate(DateTime::<Utc>::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::from_timestamp_opt(1540122067, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(1540122067, 0).unwrap(),
|
||||||
Utc,
|
Utc,
|
||||||
))
|
))
|
||||||
@ -58,11 +58,11 @@ impl super::Validator for LegacyForgeValidator {
|
|||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods
|
// Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods
|
||||||
SupportedGameVersions::Range(
|
SupportedGameVersions::Range(
|
||||||
DateTime::from_utc(
|
DateTime::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::from_timestamp_opt(1366818300, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(1366818300, 0).unwrap(),
|
||||||
Utc,
|
Utc,
|
||||||
),
|
),
|
||||||
DateTime::from_utc(
|
DateTime::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::from_timestamp_opt(1505810340, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(1505810340, 0).unwrap(),
|
||||||
Utc,
|
Utc,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -19,7 +19,7 @@ impl super::Validator for QuiltValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
SupportedGameVersions::PastDate(DateTime::from_utc(
|
SupportedGameVersions::PastDate(DateTime::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::from_timestamp_opt(1646070100, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(1646070100, 0).unwrap(),
|
||||||
Utc,
|
Utc,
|
||||||
))
|
))
|
||||||
|
|||||||
@ -20,7 +20,7 @@ impl super::Validator for PackValidator {
|
|||||||
|
|
||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// Time since release of 13w24a which replaced texture packs with resource packs
|
// Time since release of 13w24a which replaced texture packs with resource packs
|
||||||
SupportedGameVersions::PastDate(DateTime::from_utc(
|
SupportedGameVersions::PastDate(DateTime::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::from_timestamp_opt(1371137542, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(1371137542, 0).unwrap(),
|
||||||
Utc,
|
Utc,
|
||||||
))
|
))
|
||||||
@ -58,11 +58,11 @@ impl super::Validator for TexturePackValidator {
|
|||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
// a1.2.2a to 13w23b
|
// a1.2.2a to 13w23b
|
||||||
SupportedGameVersions::Range(
|
SupportedGameVersions::Range(
|
||||||
DateTime::from_utc(
|
DateTime::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::from_timestamp_opt(1289339999, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(1289339999, 0).unwrap(),
|
||||||
Utc,
|
Utc,
|
||||||
),
|
),
|
||||||
DateTime::from_utc(
|
DateTime::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::from_timestamp_opt(1370651522, 0).unwrap(),
|
NaiveDateTime::from_timestamp_opt(1370651522, 0).unwrap(),
|
||||||
Utc,
|
Utc,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -70,24 +70,6 @@ async fn user_scopes() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// User payout info writing
|
|
||||||
let failure_write_user_payout = Scopes::all() ^ Scopes::PAYOUTS_WRITE; // Failure case should include USER_WRITE
|
|
||||||
let write_user_payout = Scopes::USER_WRITE | Scopes::PAYOUTS_WRITE;
|
|
||||||
let req_gen = || {
|
|
||||||
TestRequest::patch().uri("/v2/user/user").set_json(json!( {
|
|
||||||
"payout_data": {
|
|
||||||
"payout_wallet": "paypal",
|
|
||||||
"payout_wallet_type": "email",
|
|
||||||
"payout_address": "test@modrinth.com"
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
ScopeTest::new(&test_env)
|
|
||||||
.with_failure_scopes(failure_write_user_payout)
|
|
||||||
.test(req_gen, write_user_payout)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// User deletion
|
// User deletion
|
||||||
// (The failure is first, and this is the last test for this test function, we can delete it and use the same PAT for both tests)
|
// (The failure is first, and this is the last test for this test function, we can delete it and use the same PAT for both tests)
|
||||||
let delete_user = Scopes::USER_DELETE;
|
let delete_user = Scopes::USER_DELETE;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user