mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: update auth flow (#5790)
* Backend routes for choosing username in OAuth flow * fix up oauth flow routes * improve URL-related OAuth code * Use user-provided callback addr instead of SELF_ADDR * Revert "Use user-provided callback addr instead of SELF_ADDR" This reverts commit7ea0635d86. * fix flow * fix: backend response for create oauth account * feat: new auth flow (#5840) * update auth with new designs * refactor: auth.js to auth.ts * refactor: componentize auth pages * fix: auth pages height * feat: initial implementation of new sign-in oauth * fix create account flow * fix checkbox * remove hard coded username * implement create user validation endpoint and add more specific error responses * feat: implement under 13 DOB guard and email/password validation route * fix: TOCTOU issue * refactor: pnpm prepr * fix: make sure staging uses staging * fix: hcaptcha styles * fix: copy * remove: auth/welcome page as its no longer used * refactor: bring root page card styles into individual components and use tailwind * fix: account settings modals to use new modal and fix lots of bad styles * refactor: pnpm prepr * feat: implement last signed in indicator * fix: append number when generated name from email is taken * refactor: pnpm prepr * fix: last sign in badge color * fix: qa issues * refactor: pnpm prepr * fix: hover effect on native date picker * chore: temp staging undo * Revert "chore: temp staging undo" This reverts commitcad6bd4f92. * feat: handle app create account * fix: last signed in style * fix: add initOnMounted for SSR race * refactor: use typescript * refactor: pnpm prepr * refactor: use typescript for reset-password * refactor: convert verify-email to use typescript * refactor: convert authorize.vue to use typescript * fix: authorize.vue error states * feat: small style updates * feat: implement date picker component * feat: improve UX and styles for range select * refactor: pnpm prepr * fix: range select border styles * feat: implement date picker component in create account * feat: implement preserve date for date picker * update rust toolchain * increase recursion limit * fix: date picker can be null * fix: calculate age based on user's timezone * fix: number input icons color * fix: date picker icons * feat: improve styles * fix: add width on date * fix: hover color bad on number input * fix lints * feat: add default date open view * fmt * fix: account.vue * fix: remove default date to open 13 years ago * fix: edit copy on info banner * fix: cannot hover over project card tooltip items (#6071) fix: cannot hover over project cards * feat: improve add dependency flow (#6075) * fix: shadow on nav * feat: improve add dependency flow * feat: update suggested dependency style * feat: update dependency rows to use version number and update styles * feat: implement combobox select searched text on focus * feat: add Tabs.vue * feat: update nav tabs to use tabs * feat: improve project search dropdown * fix: dependency search not clearing inbound query * fix: combobox no options open state bug * feat: improve dependency project and version search * fix: open modrinth project links in the app (#6072) * pin tanstack versions + set pnpm min age to 7 days * squash commits * fix: 2 factor auth enter code screen styles * update copy * update copy * improve reset password * feat: update sign in screen * fix: unused import * Merge branch 'main' into boris/dev-908-backend-changes * Revert "Merge branch 'main' into boris/dev-908-backend-changes" This reverts commitb9b03796e3. * fix: add stroke * feat: add passkey support (#6375) * feat: add passkey backend * feat: passkey frontend * invalidate sessions on compromised passkey * chore: run sqlx prepare * fix: make passkey button use both collumns to prevent empty space * fix: correctly verify max passkeys in finish route * fix: use structs for response * fix: add rp name default * style: use web::Json * fmt * feat: improve manage passkeys UI * fix copy * pnpm prepr --------- Co-authored-by: tdgao <mr.trumgao@gmail.com> Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: Michael H. <michael@iptables.sh> Co-authored-by: Calum H. (IMB11) <contact@cal.engineer> Co-authored-by: Calum H. <calum@modrinth.com> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com> Co-authored-by: DeDiamondPro <67508414+DeDiamondPro@users.noreply.github.com>
This commit is contained in:
co-authored by
tdgao
Truman Gao
Michael H.
Calum H.
Calum H.
Prospector
DeDiamondPro
parent
6fc741f7c0
commit
ef4044534f
@@ -173,3 +173,6 @@ MURALPAY_SOURCE_ACCOUNT_ID=00000000-0000-0000-0000-000000000000
|
||||
DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
|
||||
SERVER_PING_TIMEOUT=10000
|
||||
SERVER_PING_RETRIES=3
|
||||
|
||||
# Display name for Webauthn Authenticators
|
||||
WEBAUTHN_RP_NAME=Modrinth
|
||||
|
||||
@@ -195,3 +195,6 @@ DEFAULT_AFFILIATE_REVENUE_SPLIT=0.1
|
||||
SERVER_PING_TIMEOUT=10000
|
||||
SERVER_PING_RETRIES=3
|
||||
SERVER_PING_MIN_INTERVAL_SEC=1800
|
||||
|
||||
# Display name for Webauthn Authenticators
|
||||
WEBAUTHN_RP_NAME=Modrinth
|
||||
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Ask2AgentMigrationStateService">
|
||||
<option name="migrationStatus" value="COMPLETED" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+11
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/labrinth.iml" filepath="$PROJECT_DIR$/.idea/labrinth.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, user_id, name, credential_id,\n passkey AS \"passkey: sqlx::types::Json<Passkey>\",\n last_used, created_at\n FROM user_passkeys\n WHERE credential_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "credential_id",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "passkey: sqlx::types::Json<Passkey>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "last_used",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Bytea"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "05d26562a95715d65bbb2fd1c4163ebb067931f4c3caeb93601c98f1d533983b"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c"
|
||||
}
|
||||
Generated
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id,\n status AS \"status: PayoutStatus\"\n FROM payouts\n ORDER BY id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "status: PayoutStatus",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286"
|
||||
}
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4"
|
||||
}
|
||||
Generated
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE user_passkeys SET name = $1\n WHERE id = $2 AND user_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "29fc17743ca5cf06fcea50ebc477576b2fd7fe535afee2f3136d6b153bbf4129"
|
||||
}
|
||||
Generated
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM sessions WHERE user_id = $1 RETURNING id, session\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "session",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "31c1a2872410a5834e9995ad73cdc1cdeda4f3386e385dd66bec5be2aaa75b5b"
|
||||
}
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n (\n SELECT MAX(campaign_donations.donated_at)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_last_donated_at,\n (\n SELECT SUM(campaign_donations.amount_usd)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_total_amount_donated_usd,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
|
||||
"query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n (\n SELECT MAX(campaign_donations.donated_at)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_last_donated_at,\n (\n SELECT SUM(campaign_donations.amount_usd)\n FROM campaign_donations\n WHERE campaign_donations.user_id = users.id\n ) AS campaign_pride_26_total_amount_donated_usd,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter,\n eligibility_verified_at\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -137,6 +137,11 @@
|
||||
"ordinal": 26,
|
||||
"name": "is_subscribed_to_newsletter",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "eligibility_verified_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -172,8 +177,9 @@
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e6c22fe10d603206c8466da630b30d0d4848455f5cddbf9202d9cdbfa1f306b5"
|
||||
"hash": "3ca51012492b969bb6a474ee22c9e53e57b6da3a1145f0d86a2bab36be436eb6"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM user_passkeys\n WHERE id = $1 AND user_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "42a9ddd851497b7a340e00b8289aa56692577571ec6d21131b4a31f55d37b98e"
|
||||
}
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE user_passkeys\n SET passkey = $1, last_used = NOW()\n WHERE id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Jsonb",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "538bfc1694ce1d177ea20f269353922e836d2dfd9eb447e278f38266d13c8e73"
|
||||
}
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO payouts_values_notifications (date_available, user_id, notified)\n VALUES ($1, $2, FALSE)\n ON CONFLICT (date_available, user_id) DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850"
|
||||
}
|
||||
Generated
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)\n VALUES ($1, NULL, $2, NOW(), $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Numeric",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8"
|
||||
}
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO user_passkeys (\n id, user_id, name, credential_id, passkey, created_at, last_used\n )\n VALUES (\n $1, $2 ,$3, $4, $5, $6, $7\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Bytea",
|
||||
"Jsonb",
|
||||
"Timestamptz",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7de293b153f075b8e44bf8ec8eee0fef0bc06a8aafd844c38aff3dcbf63e1bc9"
|
||||
}
|
||||
Generated
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, user_id, name, credential_id,\n passkey AS \"passkey: sqlx::types::Json<Passkey>\",\n last_used, created_at\n FROM user_passkeys\n WHERE user_id = $1\n ORDER BY created_at DESC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "credential_id",
|
||||
"type_info": "Bytea"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "passkey: sqlx::types::Json<Passkey>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "last_used",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "806c5ed76a076bfd060fca40aa4cff8823503afefd68c2a2a7a6b879dfdaea2d"
|
||||
}
|
||||
Generated
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM user_passkeys WHERE id=$1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9851b2891716958cb2e0eb8f2deccd25d6f36dfc03b69e83079bfce0bf2030fb"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, raw_avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20, $21, $22\n )\n ",
|
||||
"query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, raw_avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter,\n eligibility_verified_at\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20, $21, $22,\n $23\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -26,10 +26,11 @@
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Bool"
|
||||
"Bool",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "010c69fa61e1329156020b251e75d46bc09344c1846b3098accce5801e571e5e"
|
||||
"hash": "a335ff4338fa4b3c1aaa9f62c61aad3c3e8bef0e68035a024995c62cfe69c568"
|
||||
}
|
||||
Generated
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "status: PayoutStatus",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3"
|
||||
}
|
||||
Generated
-18
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02"
|
||||
}
|
||||
Generated
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM user_passkeys\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ddef9fee29f75736494b196a10dfe363a86e42417a047a6ed80f2c62811d5c2d"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41"
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
- `Modrinth-Admin: feedbeef` as admin key
|
||||
- If some steps require you to create a project/mod or version for testing, ask the user to go into the web frontend and manually create a project/version
|
||||
- When using `sqlx::query` etc. always use the macro form like `sqlx::query!` or `sqlx::query_scalar!` - never the plain function form. Avoid using `query_as!`.
|
||||
- Do not use `()` as an error type for operations, unless you have a very good reason. Either make a new error type, or use `eyre::Report`.
|
||||
- Do not run `cargo test`, even for a single specific test, unless explicitly prompted to by the user, since it takes a long time to run.
|
||||
- You can force a search reindex by:
|
||||
- Running `cd apps/labrinth && cargo run -p labrinth -- --run-background-task index-search` (prefer this if backend is running locally)
|
||||
|
||||
@@ -131,6 +131,11 @@ utoipa-actix-web = { workspace = true }
|
||||
utoipa-scalar = { workspace = true, features = ["actix-web"] }
|
||||
uuid = { workspace = true, features = ["fast-rng", "serde", "v4", "v7"] }
|
||||
validator = { workspace = true, features = ["derive"] }
|
||||
webauthn-rs = { workspace = true, features = [
|
||||
"conditional-ui",
|
||||
"danger-allow-state-serialisation"
|
||||
] }
|
||||
webauthn-rs-proto = { workspace = true }
|
||||
webp = { workspace = true }
|
||||
woothee = { workspace = true }
|
||||
yaserde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN eligibility_verified_at TIMESTAMPTZ;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE user_passkeys (
|
||||
id BIGINT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
credential_id BYTEA NOT NULL UNIQUE,
|
||||
passkey JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX user_passkeys_user_id ON user_passkeys (user_id);
|
||||
@@ -46,7 +46,13 @@ pub enum AuthenticationError {
|
||||
#[error(
|
||||
"User email is already registered on Modrinth. Try 'Forgot password' to access your account."
|
||||
)]
|
||||
DuplicateUser,
|
||||
DuplicateEmail,
|
||||
#[error("Username is already taken on Modrinth.")]
|
||||
UsernameTaken,
|
||||
#[error(
|
||||
"This authentication provider is already linked to another Modrinth account."
|
||||
)]
|
||||
ProviderAlreadyLinked,
|
||||
#[error("Invalid state sent, you probably need to get a new websocket")]
|
||||
SocketError,
|
||||
#[error("Invalid callback URL specified")]
|
||||
@@ -76,7 +82,11 @@ impl actix_web::ResponseError for AuthenticationError {
|
||||
AuthenticationError::FileHosting(..) => {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
AuthenticationError::DuplicateUser => StatusCode::BAD_REQUEST,
|
||||
AuthenticationError::DuplicateEmail => StatusCode::BAD_REQUEST,
|
||||
AuthenticationError::UsernameTaken => StatusCode::BAD_REQUEST,
|
||||
AuthenticationError::ProviderAlreadyLinked => {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
AuthenticationError::SocketError => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
@@ -105,7 +115,11 @@ impl AuthenticationError {
|
||||
AuthenticationError::InvalidClientId => "invalid_client_id",
|
||||
AuthenticationError::Url => "url_error",
|
||||
AuthenticationError::FileHosting(..) => "file_hosting",
|
||||
AuthenticationError::DuplicateUser => "duplicate_user",
|
||||
AuthenticationError::DuplicateEmail => "duplicate_email",
|
||||
AuthenticationError::UsernameTaken => "username_taken",
|
||||
AuthenticationError::ProviderAlreadyLinked => {
|
||||
"provider_already_linked"
|
||||
}
|
||||
AuthenticationError::SocketError => "socket",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use super::ids::*;
|
||||
use crate::auth::AuthProvider;
|
||||
use crate::auth::oauth::uris::OAuthRedirectUris;
|
||||
use crate::database::models::DatabaseError;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::{auth::AuthProvider, routes::internal::flows::TempUser};
|
||||
use chrono::Duration;
|
||||
use rand::Rng;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use webauthn_rs::prelude::{DiscoverableAuthentication, PasskeyRegistration};
|
||||
|
||||
const FLOWS_NAMESPACE: &str = "flows";
|
||||
|
||||
@@ -18,10 +20,15 @@ const FLOWS_NAMESPACE: &str = "flows";
|
||||
pub enum DBFlow {
|
||||
OAuth {
|
||||
user_id: Option<DBUserId>,
|
||||
url: String,
|
||||
url: Url,
|
||||
provider: AuthProvider,
|
||||
existing_user_id: Option<DBUserId>,
|
||||
},
|
||||
OAuthPending {
|
||||
url: Url,
|
||||
provider: AuthProvider,
|
||||
user: TempUser,
|
||||
},
|
||||
Login2FA {
|
||||
user_id: DBUserId,
|
||||
},
|
||||
@@ -52,31 +59,48 @@ pub enum DBFlow {
|
||||
scopes: Scopes,
|
||||
original_redirect_uri: Option<String>, // Needed for https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
||||
},
|
||||
RegisterPasskey {
|
||||
user_id: DBUserId,
|
||||
state: PasskeyRegistration,
|
||||
},
|
||||
AuthenticatePasskey {
|
||||
state: DiscoverableAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
impl DBFlow {
|
||||
pub async fn insert_with_state(
|
||||
&self,
|
||||
expires: Duration,
|
||||
redis: &RedisPool,
|
||||
state: &str,
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
FLOWS_NAMESPACE,
|
||||
&state,
|
||||
&self,
|
||||
Some(expires.num_seconds()),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert(
|
||||
&self,
|
||||
expires: Duration,
|
||||
redis: &RedisPool,
|
||||
) -> Result<String, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let flow = ChaCha20Rng::from_entropy()
|
||||
let state = ChaCha20Rng::from_entropy()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
FLOWS_NAMESPACE,
|
||||
&flow,
|
||||
&self,
|
||||
Some(expires.num_seconds()),
|
||||
)
|
||||
.await?;
|
||||
Ok(flow)
|
||||
self.insert_with_state(expires, redis, &state).await?;
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::models::ids::{
|
||||
AffiliateCodeId, AnalyticsEventId, AttributionGroupId, CampaignDonationId,
|
||||
ChargeId, CollectionId, FileId, ImageId, NotificationId,
|
||||
OAuthAccessTokenId, OAuthClientAuthorizationId, OAuthClientId,
|
||||
OAuthRedirectUriId, OrganizationId, PatId, PayoutId, ProductId,
|
||||
OAuthRedirectUriId, OrganizationId, PasskeyId, PatId, PayoutId, ProductId,
|
||||
ProductPriceId, ProjectId, ReportId, SessionId, SharedInstanceId,
|
||||
SharedInstanceVersionId, TeamId, TeamMemberId, ThreadId, ThreadMessageId,
|
||||
UserSubscriptionId, VersionId,
|
||||
@@ -282,6 +282,10 @@ db_id_interface!(
|
||||
AnalyticsEventId,
|
||||
generator: generate_analytics_event_id @ "analytics_events",
|
||||
);
|
||||
db_id_interface!(
|
||||
PasskeyId,
|
||||
generator: generate_passkey_id @ "user_passkeys",
|
||||
);
|
||||
|
||||
id_type!(CategoryId as i32);
|
||||
id_type!(GameId as i32);
|
||||
|
||||
@@ -23,6 +23,7 @@ pub mod oauth_client_authorization_item;
|
||||
pub mod oauth_client_item;
|
||||
pub mod oauth_token_item;
|
||||
pub mod organization_item;
|
||||
pub mod passkey_item;
|
||||
pub mod pat_item;
|
||||
pub mod payout_item;
|
||||
pub mod payouts_values_notifications;
|
||||
@@ -51,6 +52,7 @@ pub use ids::*;
|
||||
pub use image_item::DBImage;
|
||||
pub use oauth_client_item::DBOAuthClient;
|
||||
pub use organization_item::DBOrganization;
|
||||
pub use passkey_item::DBPasskey;
|
||||
pub use project_item::DBProject;
|
||||
pub use team_item::DBTeam;
|
||||
pub use team_item::DBTeamMember;
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
use super::ids::*;
|
||||
use crate::database::PgTransaction;
|
||||
use crate::database::models::DatabaseError;
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::Json;
|
||||
use webauthn_rs::prelude::Passkey;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct DBPasskey {
|
||||
pub id: DBPasskeyId,
|
||||
pub user_id: DBUserId,
|
||||
pub name: String,
|
||||
pub credential_id: Vec<u8>,
|
||||
pub passkey: Passkey,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub last_used: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl DBPasskey {
|
||||
pub async fn insert(
|
||||
&self,
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO user_passkeys (
|
||||
id, user_id, name, credential_id, passkey, created_at, last_used
|
||||
)
|
||||
VALUES (
|
||||
$1, $2 ,$3, $4, $5, $6, $7
|
||||
)
|
||||
",
|
||||
self.id as DBPasskeyId,
|
||||
self.user_id as DBUserId,
|
||||
self.name,
|
||||
self.credential_id,
|
||||
Json(&self.passkey) as _,
|
||||
self.created_at,
|
||||
self.last_used,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_by_credential_id<'a, E>(
|
||||
credential_id: &[u8],
|
||||
exec: E,
|
||||
) -> Result<Option<DBPasskey>, DatabaseError>
|
||||
where
|
||||
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT id, user_id, name, credential_id,
|
||||
passkey AS "passkey: sqlx::types::Json<Passkey>",
|
||||
last_used, created_at
|
||||
FROM user_passkeys
|
||||
WHERE credential_id = $1
|
||||
"#,
|
||||
credential_id,
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?
|
||||
.map(|x| DBPasskey {
|
||||
id: DBPasskeyId(x.id),
|
||||
user_id: DBUserId(x.user_id),
|
||||
name: x.name,
|
||||
credential_id: x.credential_id,
|
||||
passkey: x.passkey.0,
|
||||
created_at: x.created_at,
|
||||
last_used: x.last_used,
|
||||
});
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn get_for_user<'a, E>(
|
||||
user_id: DBUserId,
|
||||
exec: E,
|
||||
) -> Result<Vec<DBPasskey>, DatabaseError>
|
||||
where
|
||||
E: crate::database::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let passkeys = sqlx::query!(
|
||||
r#"
|
||||
SELECT id, user_id, name, credential_id,
|
||||
passkey AS "passkey: sqlx::types::Json<Passkey>",
|
||||
last_used, created_at
|
||||
FROM user_passkeys
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
"#,
|
||||
user_id.0,
|
||||
)
|
||||
.fetch(exec)
|
||||
.map_ok(|x| DBPasskey {
|
||||
id: DBPasskeyId(x.id),
|
||||
user_id: DBUserId(x.user_id),
|
||||
name: x.name,
|
||||
credential_id: x.credential_id,
|
||||
passkey: x.passkey.0,
|
||||
created_at: x.created_at,
|
||||
last_used: x.last_used,
|
||||
})
|
||||
.try_collect::<Vec<DBPasskey>>()
|
||||
.await?;
|
||||
|
||||
Ok(passkeys)
|
||||
}
|
||||
|
||||
pub async fn rename(
|
||||
id: DBPasskeyId,
|
||||
user_id: DBUserId,
|
||||
name: &str,
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
UPDATE user_passkeys SET name = $1
|
||||
WHERE id = $2 AND user_id = $3
|
||||
",
|
||||
name,
|
||||
id as DBPasskeyId,
|
||||
user_id as DBUserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn update_after_auth(
|
||||
id: DBPasskeyId,
|
||||
passkey: Passkey,
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
UPDATE user_passkeys
|
||||
SET passkey = $1, last_used = NOW()
|
||||
WHERE id = $2
|
||||
",
|
||||
Json(&passkey) as _,
|
||||
id as DBPasskeyId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
id: DBPasskeyId,
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM user_passkeys
|
||||
WHERE id = $1
|
||||
",
|
||||
id as DBPasskeyId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
pub async fn remove_for_user(
|
||||
id: DBPasskeyId,
|
||||
user_id: DBUserId,
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<bool, DatabaseError> {
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM user_passkeys
|
||||
WHERE id = $1 AND user_id = $2
|
||||
",
|
||||
id as DBPasskeyId,
|
||||
user_id as DBUserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use crate::database::redis::RedisPool;
|
||||
use ariadne::ids::base62_impl::parse_base62;
|
||||
use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use futures_util::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
@@ -311,4 +312,22 @@ impl DBSession {
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
pub async fn remove_all_for_user(
|
||||
user_id: DBUserId,
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
) -> Result<Vec<(DBSessionId, String)>, sqlx::Error> {
|
||||
let sessions = sqlx::query!(
|
||||
"
|
||||
DELETE FROM sessions WHERE user_id = $1 RETURNING id, session
|
||||
",
|
||||
user_id.0
|
||||
)
|
||||
.fetch(&mut *transaction)
|
||||
.map_ok(|x| (DBSessionId(x.id), x.session))
|
||||
.try_collect()
|
||||
.await?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ pub struct DBUser {
|
||||
pub allow_friend_requests: bool,
|
||||
|
||||
pub is_subscribed_to_newsletter: bool,
|
||||
|
||||
pub eligibility_verified_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
@@ -83,13 +85,15 @@ impl DBUser {
|
||||
avatar_url, raw_avatar_url, bio, created,
|
||||
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
||||
email_verified, password, paypal_id, paypal_country, paypal_email,
|
||||
venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter
|
||||
venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter,
|
||||
eligibility_verified_at
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7,
|
||||
$8, $9, $10, $11, $12, $13,
|
||||
$14, $15, $16, $17, $18, $19, $20, $21, $22
|
||||
$14, $15, $16, $17, $18, $19, $20, $21, $22,
|
||||
$23
|
||||
)
|
||||
",
|
||||
self.id as DBUserId,
|
||||
@@ -114,6 +118,7 @@ impl DBUser {
|
||||
self.stripe_customer_id,
|
||||
self.allow_friend_requests,
|
||||
self.is_subscribed_to_newsletter,
|
||||
self.eligibility_verified_at,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
@@ -209,7 +214,8 @@ impl DBUser {
|
||||
) AS campaign_pride_26_total_amount_donated_usd,
|
||||
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
||||
email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,
|
||||
venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter
|
||||
venmo_handle, stripe_customer_id, allow_friend_requests, is_subscribed_to_newsletter,
|
||||
eligibility_verified_at
|
||||
FROM users
|
||||
WHERE id = ANY($1) OR LOWER(username) = ANY($2)
|
||||
",
|
||||
@@ -259,6 +265,7 @@ impl DBUser {
|
||||
totp_secret: u.totp_secret,
|
||||
allow_friend_requests: u.allow_friend_requests,
|
||||
is_subscribed_to_newsletter: u.is_subscribed_to_newsletter,
|
||||
eligibility_verified_at: u.eligibility_verified_at,
|
||||
};
|
||||
|
||||
acc.insert(u.id, (Some(u.username), user));
|
||||
|
||||
@@ -311,4 +311,6 @@ vars! {
|
||||
SERVER_PING_MIN_INTERVAL_SEC: u64 = 30u64 * 60;
|
||||
SERVER_PING_TIMEOUT_MS: u64 = 3u64 * 1000;
|
||||
SERVER_PING_MAX_FAIL_COUNT: u64 = 3u64;
|
||||
|
||||
WEBAUTHN_RP_NAME: String = "Modrinth";
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ use crate::util::http::HttpClient;
|
||||
use crate::util::ratelimit::{AsyncRateLimiter, GCRAParameters};
|
||||
use crate::util::tiltify::TiltifyClient;
|
||||
use sync::friends::handle_pubsub;
|
||||
use url::Url;
|
||||
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||
|
||||
pub mod auth;
|
||||
pub mod background_task;
|
||||
@@ -76,6 +78,7 @@ pub struct LabrinthConfig {
|
||||
pub http_client: web::Data<HttpClient>,
|
||||
pub tiltify_client: web::Data<TiltifyClient>,
|
||||
pub kafka_client: web::Data<util::kafka::KafkaClientState>,
|
||||
pub webauthn: web::Data<Webauthn>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -312,6 +315,19 @@ pub fn app_setup(
|
||||
});
|
||||
}
|
||||
|
||||
let webauthn_origin = Url::parse(&ENV.SITE_URL).expect("invalid SITE_URL");
|
||||
let webauthn_rp_id = webauthn_origin
|
||||
.host_str()
|
||||
.expect("SITE_URL has no host")
|
||||
.to_string();
|
||||
let webauthn = web::Data::new(
|
||||
WebauthnBuilder::new(&webauthn_rp_id, &webauthn_origin)
|
||||
.expect("invalid webauthn configuration")
|
||||
.rp_name(&ENV.WEBAUTHN_RP_NAME)
|
||||
.build()
|
||||
.expect("failed to build webauthn"),
|
||||
);
|
||||
|
||||
LabrinthConfig {
|
||||
pool,
|
||||
ro_pool,
|
||||
@@ -338,6 +354,7 @@ pub fn app_setup(
|
||||
.expect("ARCHON_URL and PYRO_API_KEY must be set"),
|
||||
),
|
||||
email_queue: web::Data::new(email_queue),
|
||||
webauthn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +398,7 @@ pub fn app_config(
|
||||
.app_data(labrinth_config.rate_limiter.clone())
|
||||
.app_data(labrinth_config.kafka_client.clone())
|
||||
.app_data(labrinth_config.search_state.clone())
|
||||
.app_data(labrinth_config.webauthn.clone())
|
||||
.configure(routes::v3::config)
|
||||
.configure(routes::internal::config)
|
||||
.configure(routes::root_config)
|
||||
|
||||
@@ -29,3 +29,4 @@ base62_id!(UserSubscriptionId);
|
||||
base62_id!(VersionId);
|
||||
base62_id!(AffiliateCodeId);
|
||||
base62_id!(AnalyticsEventId);
|
||||
base62_id!(PasskeyId);
|
||||
|
||||
@@ -66,6 +66,7 @@ pub struct User {
|
||||
pub payout_data: Option<UserPayoutData>,
|
||||
pub stripe_customer_id: Option<String>,
|
||||
pub allow_friend_requests: Option<bool>,
|
||||
pub eligibility_verified_at: Option<DateTime<Utc>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub moderation_notes: Option<Option<ModerationNote>>,
|
||||
|
||||
@@ -120,6 +121,7 @@ impl From<DBUser> for User {
|
||||
github_id: None,
|
||||
stripe_customer_id: None,
|
||||
allow_friend_requests: None,
|
||||
eligibility_verified_at: None,
|
||||
moderation_notes: None,
|
||||
}
|
||||
}
|
||||
@@ -186,6 +188,7 @@ impl User {
|
||||
}),
|
||||
stripe_customer_id: db_user.stripe_customer_id,
|
||||
allow_friend_requests: Some(db_user.allow_friend_requests),
|
||||
eligibility_verified_at: db_user.eligibility_verified_at,
|
||||
moderation_notes: None,
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::database::{PgPool, ReadOnlyPgPool};
|
||||
@@ -88,7 +89,7 @@ pub async fn forge_updates(
|
||||
)
|
||||
.await?;
|
||||
|
||||
versions.sort_by_key(|b| std::cmp::Reverse(b.date_published));
|
||||
versions.sort_by_key(|b| Reverse(b.date_published));
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ForgeUpdates {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::any::type_name;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::auth::checks::{filter_visible_versions, is_visible_project};
|
||||
@@ -1389,10 +1390,10 @@ pub async fn dependency_list_internal(
|
||||
)
|
||||
.await?;
|
||||
|
||||
projects.sort_by_key(|b| std::cmp::Reverse(b.published));
|
||||
projects.sort_by_key(|b| Reverse(b.published));
|
||||
projects.dedup_by(|a, b| a.id == b.id);
|
||||
|
||||
versions.sort_by_key(|b| std::cmp::Reverse(b.date_published));
|
||||
versions.sort_by_key(|b| Reverse(b.date_published));
|
||||
versions.dedup_by(|a, b| a.id == b.id);
|
||||
|
||||
Ok(HttpResponse::Ok().json(DependencyInfo { projects, versions }))
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::{HashMap, HashSet},
|
||||
};
|
||||
|
||||
use super::{ApiError, oauth_clients::get_user_clients};
|
||||
use crate::database::PgPool;
|
||||
@@ -1078,7 +1081,7 @@ pub async fn user_notifications(
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
notifications.sort_by_key(|b| std::cmp::Reverse(b.created));
|
||||
notifications.sort_by_key(|b| Reverse(b.created));
|
||||
Ok(HttpResponse::Ok().json(notifications))
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
|
||||
Reference in New Issue
Block a user