refactor(tranquil-pds): consolidate repos, rate limits, validation, and auth helpers

This commit is contained in:
Lewis
2026-03-22 12:51:44 +00:00
committed by Tangled
parent 318818ad51
commit 19b0ea19c6
72 changed files with 1678 additions and 406 deletions
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT t.token FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "05fd99170e31e68fa5028c862417cdf535cd70e09fde0a8a28249df0070eb2fc"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET deactivated_at = $1 WHERE did = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Timestamptz",
"Text"
]
},
"nullable": []
},
"hash": "0710b57fb9aa933525f617b15e6e2e5feaa9c59c38ec9175568abdacda167107"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "body",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "0ec60bb854a4991d0d7249a68f7445b65c8cc8c723baca221d85f5e4f2478b99"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token FROM sso_pending_registration WHERE token = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "0fae1be7a75bdc58c69a9af97cad4aec23c32a9378764b8d6d7eb2cc89c562b1"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "1c84643fd6bc57c76517849a64d2d877df337e823d4c2c2b077f695bbfc9e9ac"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n RETURNING token, request_uri\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "request_uri",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "24b823043ab60f36c29029137fef30dfe33922bb06067f2fdbfc1fbb4b0a2a81"
}
@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id)\n VALUES ($1, $2, $3)\n RETURNING id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": [
false
]
},
"hash": "2841093a67480e75e1e9e4046bf3eb74afae2d04f5ea0ec17a4d433983e6d71c"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "29ef76852bb89af1ab9e679ceaa4abcf8bc8268a348d3be0da9840d1708d20b5"
}
@@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "376b72306b50f747bc9161985ff4f50c35c53025a55ccf5e9933dc3795d29313"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM external_identities WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "3933ea5b147ab6294936de147b98e116cfae848ecd76ea5d367585eb5117f2ad"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO users (did, handle, email, password_hash) VALUES ($1, $2, $3, 'hash')",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "3bed8d4843545f4a9676207513806603c50eb2af92957994abaf1c89c0294c12"
}
@@ -0,0 +1,55 @@
{
"db_name": "PostgreSQL",
"query": "SELECT subject, body, comms_type as \"comms_type: String\" FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' ORDER BY created_at DESC LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "subject",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "body",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "comms_type: String",
"type_info": {
"Custom": {
"name": "comms_type",
"kind": {
"Enum": [
"welcome",
"email_verification",
"password_reset",
"email_update",
"account_deletion",
"admin_email",
"plc_operation",
"two_factor_code",
"channel_verification",
"passkey_recovery",
"legacy_login_alert",
"migration_verification",
"channel_verified"
]
}
}
}
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true,
false,
false
]
},
"hash": "4445cc86cdf04894b340e67661b79a3c411917144a011f50849b737130b24dbe"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM users WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "4560c237741ce9d4166aecd669770b3360a3ac71e649b293efb88d92c3254068"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token, expires_at FROM account_deletion_requests WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "47fe4a54857344d8f789f37092a294cd58f64b4fb431b54b5deda13d64525e88"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email_verified FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email_verified",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "49cbc923cc4a0dcf7dea4ead5ab9580ff03b717586c4ca2d5343709e2dac86b6"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT token FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "4fef326fa2d03d04869af3fec702c901d1ecf392545a3a032438b2c1859d46cc"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO oauth_authorization_request (id, client_id, parameters, expires_at)\n VALUES ($1, 'https://test.example.com', $2, NOW() + INTERVAL '1 hour')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Jsonb"
]
},
"nullable": []
},
"hash": "575c1e5529874f8f523e6fe22ccf4ee3296806581b1765dfb91a84ffab347f15"
}
@@ -0,0 +1,33 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "596c3400a60c77c7645fd46fcea61fa7898b6832e58c0f647f382b23b81d350e"
}
@@ -0,0 +1,81 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, did, provider as \"provider: SsoProviderType\", provider_user_id, provider_username, provider_email\n FROM external_identities\n WHERE provider = $1 AND provider_user_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "provider: SsoProviderType",
"type_info": {
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
}
},
{
"ordinal": 3,
"name": "provider_user_id",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "provider_username",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "provider_email",
"type_info": "Text"
}
],
"parameters": {
"Left": [
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true
]
},
"hash": "59e63c5cf92985714e9586d1ce012efef733d4afaa4ea09974daf8303805e5d2"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT k.key_bytes, k.encryption_version\n FROM user_keys k\n JOIN users u ON k.user_id = u.id\n WHERE u.did = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "key_bytes",
"type_info": "Bytea"
},
{
"ordinal": 1,
"name": "encryption_version",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "5a016f289caf75177731711e56e92881ba343c73a9a6e513e205c801c5943ec0"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM sso_auth_state\n WHERE state = $1 AND expires_at > NOW()\n RETURNING state\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "state",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "5af4a386c1632903ad7102551a5bd148bcf541baab6a84c8649666a695f9c4d1"
}
@@ -0,0 +1,43 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT provider_user_id, provider_email_verified\n FROM external_identities\n WHERE did = $1 AND provider = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "provider_user_id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "provider_email_verified",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
}
]
},
"nullable": [
false,
false
]
},
"hash": "5e4c0dd92ac3c4b5e2eae5d129f2649cf3a8f068105f44a8dca9625427affc06"
}
@@ -0,0 +1,33 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_auth_state (state, request_uri, provider, action, nonce, code_verifier)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "5e9c6ec72c2c0ea1c8dff551d01baddd1dd953c828a5656db2ee39dea996f890"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT did, email_verified FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "email_verified",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "63f6f2a89650794fe90e10ce7fc785a6b9f7d37c12b31a6ff13f7c5214eef19e"
}
@@ -0,0 +1,66 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT state, request_uri, provider as \"provider: SsoProviderType\", action, nonce, code_verifier\n FROM sso_auth_state\n WHERE state = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "state",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "request_uri",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "provider: SsoProviderType",
"type_info": {
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
}
},
{
"ordinal": 3,
"name": "action",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "nonce",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "code_verifier",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true
]
},
"hash": "6c7ace2a64848adc757af6c93b9162e1d95788b372370a7ad0d7540338bb73ee"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT state FROM sso_auth_state WHERE state = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "state",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "6fbcff0206599484bfb6cef165b6f729d27e7a342f7718ee4ac07f0ca94412ba"
}
@@ -0,0 +1,33 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "712459c27fc037f45389e2766cf1057e86e93ef756a784ed12beb453b03c5da1"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT subject FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' AND body = 'Email without subject' LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "subject",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true
]
},
"hash": "785a864944c5939331704c71b0cd3ed26ffdd64f3fd0f26ecc28b6a0557bbe8f"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT t.token\n FROM plc_operation_tokens t\n JOIN users u ON t.user_id = u.id\n WHERE u.did = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "7caa8f9083b15ec1209dda35c4c6f6fba9fe338e4a6a10636b5389d426df1631"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT provider_username, last_login_at FROM external_identities WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "provider_username",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "last_login_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
true,
true
]
},
"hash": "7d24e744a4e63570b1410e50b45b745ce8915ab3715b3eff7efc2d84f27735d0"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT t.token, t.expires_at FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "82717b6f61cd79347e1ca7e92c4413743ba168d1e0d8b85566711e54d4048f81"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, provider_username, provider_email, provider_email_verified)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "85ffc37a77af832d7795f5f37efe304fced4bf56b4f2287fe9aeb3fc97e1b191"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "body",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "9ad422bf3c43e3cfd86fc88c73594246ead214ca794760d3fe77bb5cf4f27be5"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT did, public_key_did_key FROM reserved_signing_keys WHERE public_key_did_key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "did",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "public_key_did_key",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
false
]
},
"hash": "9b035b051769e6b9d45910a8bb42ac0f84c73de8c244ba4560f004ee3f4b7002"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id FROM external_identities WHERE did = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "9dba64081d4f95b5490c9a9bf30a7175db3429f39df4f25e212f38f33882fc65"
}
@@ -0,0 +1,66 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT token, request_uri, provider as \"provider: SsoProviderType\", provider_user_id,\n provider_username, provider_email\n FROM sso_pending_registration\n WHERE token = $1 AND expires_at > NOW()\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "request_uri",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "provider: SsoProviderType",
"type_info": {
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
}
},
{
"ordinal": 3,
"name": "provider_user_id",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "provider_username",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "provider_email",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true,
true
]
},
"hash": "9fd56986c1c843d386d1e5884acef8573eb55a3e9f5cb0122fcf8b93d6d667a5"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT private_key_bytes, expires_at, used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "private_key_bytes",
"type_info": "Bytea"
},
{
"ordinal": 1,
"name": "expires_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "used_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "a23a390659616779d7dbceaa3b5d5171e70fa25e3b8393e142cebcbff752f0f5"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE external_identities\n SET provider_username = $2, last_login_at = NOW()\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "a3d549a32e76c24e265c73a98dd739067623f275de0740bd576ee288f4444496"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT token FROM account_deletion_requests WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "token",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "a802d7d860f263eace39ce82bb27b633cec7287c1cc177f0e1d47ec6571564d5"
}
@@ -0,0 +1,40 @@
{
"db_name": "PostgreSQL",
"query": "SELECT preferred_comms_channel as \"preferred_comms_channel: String\", discord_username FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "preferred_comms_channel: String",
"type_info": {
"Custom": {
"name": "comms_channel",
"kind": {
"Enum": [
"email",
"discord",
"telegram",
"signal"
]
}
}
}
},
{
"ordinal": 1,
"name": "discord_username",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "a844774d8dd3c50c5faf3de5d43f534b80234759c8437434e467ca33ea10fd1f"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM sso_auth_state\n WHERE state = $1 AND expires_at > NOW()\n RETURNING state, request_uri\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "state",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "request_uri",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "aee3e8e1d8924d41bec7d866e274f8bb2ddef833eb03326103c2d0a17ee56154"
}
@@ -0,0 +1,31 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_pending_registration (token, request_uri, provider, provider_user_id, expires_at)\n VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": []
},
"hash": "ba9684872fad5201b8504c2606c29364a2df9631fe98817e7bfacd3f3f51f6cb"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM sso_auth_state WHERE expires_at < NOW()",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "bb4460f75d30f48b79d71b97f2c7d54190260deba2d2ade177dbdaa507ab275b"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT password_reset_code FROM users WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "password_reset_code",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "cd3b8098ad4c1056c1d23acd8a6b29f7abfe18ee6f559bd94ab16274b1cfdfee"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as \"count!\" FROM plc_operation_tokens t JOIN users u ON t.user_id = u.id WHERE u.did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "cda68f9b6c60295a196fc853b70ec5fd51a8ffaa2bac5942c115c99d1cbcafa3"
}
@@ -0,0 +1,31 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO sso_auth_state (state, request_uri, provider, action, expires_at)\n VALUES ($1, $2, $3, $4, NOW() - INTERVAL '1 hour')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": []
},
"hash": "d0d4fb4b44cda3442b20037b4d5efaa032e1d004c775e2b6077c5050d7d62041"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "d529d6dc9858c1da360f0417e94a3b40041b043bae57e95002d4bf5df46a4ab4"
}
@@ -0,0 +1,40 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id, provider_username, provider_email)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text",
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "dd7d80d4d118a5fc95b574e2ca9ffaccf974e52fb6ac368f716409c55f9d3ab0"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "e20cbe2a939d790aaea718b084a80d8ede655ba1cc0fd4346d7e91d6de7d6cf3"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "used_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "e64cd36284d10ab7f3d9f6959975a1a627809f444b0faff7e611d985f31b90e9"
}
@@ -0,0 +1,30 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO external_identities (did, provider, provider_user_id)\n VALUES ($1, $2, $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "sso_provider_type",
"kind": {
"Enum": [
"github",
"discord",
"google",
"gitlab",
"oidc",
"apple"
]
}
}
},
"Text"
]
},
"nullable": []
},
"hash": "eb54d2ce02cab7c2e7f9926bd469b19e5f0513f47173b2738fc01a57082d7abb"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM external_identities WHERE id = $1 AND did = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "ec22a8cc89e480c403a239eac44288e144d83364129491de6156760616666d3d"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "f26c13023b47b908ec96da2e6b8bf8b34ca6a2246c20fc96f76f0e95530762a7"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET is_admin = TRUE WHERE did = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "f29da3bdfbbc547b339b4cdb059fac26435b0feec65cf1c56f851d1c4d6b1814"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "password_reset_code",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "password_reset_code_expires_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
true
]
},
"hash": "f7af28963099aec12cf1d4f8a9a03699bb3a90f39bc9c4c0f738a37827e8f382"
}
-4
View File
@@ -56,7 +56,6 @@ pub enum ApiError {
TotpAlreadyEnabled,
TotpNotEnabled,
InvalidCode(Option<String>),
InvalidChannel,
IdentifierMismatch,
NoPasskeys,
NoChallengeInProgress,
@@ -182,7 +181,6 @@ impl ApiError {
| Self::InvalidCollection
| Self::InvalidRecord(_)
| Self::TotpNotEnabled
| Self::InvalidChannel
| Self::IdentifierMismatch
| Self::NoPasskeys
| Self::NoChallengeInProgress
@@ -272,7 +270,6 @@ impl ApiError {
Self::TotpAlreadyEnabled => Cow::Borrowed("TotpAlreadyEnabled"),
Self::TotpNotEnabled => Cow::Borrowed("TotpNotEnabled"),
Self::InvalidCode(_) => Cow::Borrowed("InvalidCode"),
Self::InvalidChannel => Cow::Borrowed("InvalidChannel"),
Self::IdentifierMismatch => Cow::Borrowed("IdentifierMismatch"),
Self::NoPasskeys => Cow::Borrowed("NoPasskeys"),
Self::NoChallengeInProgress => Cow::Borrowed("NoChallengeInProgress"),
@@ -390,7 +387,6 @@ impl ApiError {
Self::Forbidden => "Forbidden".into(),
Self::InvitesDisabled => "Invite codes are disabled on this server".into(),
Self::InvalidCollection => "Invalid collection".into(),
Self::InvalidChannel => "Invalid notification channel".into(),
Self::TotpAlreadyEnabled => "TOTP is already enabled".into(),
Self::TotpNotEnabled => "TOTP is not enabled".into(),
Self::DuplicateAppPassword => "An app password with this name already exists".into(),
+1 -1
View File
@@ -8,7 +8,7 @@ pub use error::ApiError;
pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_limit};
pub use responses::{
AccountsOutput, AuditLogOutput, ControllersOutput, DidResponse, EmailUpdateStatusOutput,
EmptyResponse, EnabledResponse, HasPasswordResponse, InUseOutput, OptionsResponse,
EmptyResponse, HasPasswordResponse, InUseOutput, OptionsResponse,
PasswordResetOutput, PreferredLocaleOutput, PresetsOutput, StatusResponse, SuccessResponse,
TokenRequiredResponse, VerifiedResponse,
};
+3 -3
View File
@@ -229,8 +229,8 @@ async fn proxy_handler(
let http_uri = crate::util::build_full_url(&format!("/xrpc{}", uri));
match crate::auth::validate_token_with_dpop(
state.user_repo.as_ref(),
state.oauth_repo.as_ref(),
state.repos.user.as_ref(),
state.repos.oauth.as_ref(),
&token,
extracted.scheme,
dpop_proof,
@@ -252,7 +252,7 @@ async fn proxy_handler(
let key_bytes = match auth_user.key_bytes {
Some(kb) => kb,
None => match state.user_repo.get_user_info_by_did(&auth_user.did).await {
None => match state.repos.user.get_user_info_by_did(&auth_user.did).await {
Ok(Some(info)) => match info.key_bytes {
Some(key_bytes_enc) => {
match crate::config::decrypt_key(
-23
View File
@@ -70,17 +70,6 @@ impl VerifiedResponse {
}
}
#[derive(Debug, Serialize)]
pub struct EnabledResponse {
pub enabled: bool,
}
impl EnabledResponse {
pub fn response(enabled: bool) -> impl IntoResponse {
Json(Self { enabled })
}
}
#[derive(Debug, Serialize)]
pub struct StatusResponse {
pub status: String,
@@ -94,18 +83,6 @@ impl StatusResponse {
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DidDocumentResponse {
pub did_document: serde_json::Value,
}
impl DidDocumentResponse {
pub fn response(did_document: serde_json::Value) -> impl IntoResponse {
Json(Self { did_document })
}
}
#[derive(Debug, Serialize)]
pub struct OptionsResponse<T: Serialize> {
pub options: T,
+1 -150
View File
@@ -1,6 +1,4 @@
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::Deref;
pub const MAX_EMAIL_LENGTH: usize = 254;
pub const MAX_LOCAL_PART_LENGTH: usize = 64;
@@ -12,56 +10,6 @@ pub const MIN_HANDLE_LENGTH: usize = 3;
pub const MAX_HANDLE_LENGTH: usize = 253;
pub const MAX_SERVICE_HANDLE_LOCAL_PART: usize = 18;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ValidatedLocalHandle(String);
impl ValidatedLocalHandle {
pub fn new(handle: impl AsRef<str>) -> Result<Self, HandleValidationError> {
let validated = validate_short_handle(handle.as_ref())?;
Ok(Self(validated))
}
pub fn new_allow_reserved(handle: impl AsRef<str>) -> Result<Self, HandleValidationError> {
let validated = validate_service_handle(handle.as_ref(), ReservedHandlePolicy::Allow)?;
Ok(Self(validated))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl Deref for ValidatedLocalHandle {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for ValidatedLocalHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<String> for ValidatedLocalHandle {
type Error = HandleValidationError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<ValidatedLocalHandle> for String {
fn from(handle: ValidatedLocalHandle) -> Self {
handle.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EmailValidationError {
Empty,
@@ -99,66 +47,6 @@ impl fmt::Display for EmailValidationError {
impl std::error::Error for EmailValidationError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ValidatedEmail(String);
impl ValidatedEmail {
pub fn new(email: impl AsRef<str>) -> Result<Self, EmailValidationError> {
let email = email.as_ref().trim();
validate_email_detailed(email)?;
Ok(Self(email.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
pub fn local_part(&self) -> &str {
self.0
.rsplit_once('@')
.map(|(local, _)| local)
.unwrap_or("")
}
pub fn domain(&self) -> &str {
self.0
.rsplit_once('@')
.map(|(_, domain)| domain)
.unwrap_or("")
}
}
impl Deref for ValidatedEmail {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for ValidatedEmail {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<String> for ValidatedEmail {
type Error = EmailValidationError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<ValidatedEmail> for String {
fn from(email: ValidatedEmail) -> Self {
email.0
}
}
fn validate_email_detailed(email: &str) -> Result<(), EmailValidationError> {
if email.is_empty() {
return Err(EmailValidationError::Empty);
@@ -386,44 +274,7 @@ pub fn validate_service_handle(
}
pub fn is_valid_email(email: &str) -> bool {
let email = email.trim();
if email.is_empty() || email.len() > MAX_EMAIL_LENGTH {
return false;
}
let parts: Vec<&str> = email.rsplitn(2, '@').collect();
if parts.len() != 2 {
return false;
}
let domain = parts[0];
let local = parts[1];
if local.is_empty() || local.len() > MAX_LOCAL_PART_LENGTH {
return false;
}
if local.starts_with('.') || local.ends_with('.') {
return false;
}
if local.contains("..") {
return false;
}
if !local
.chars()
.all(|c| c.is_ascii_alphanumeric() || EMAIL_LOCAL_SPECIAL_CHARS.contains(c))
{
return false;
}
if domain.is_empty() || domain.len() > MAX_DOMAIN_LENGTH {
return false;
}
if !domain.contains('.') {
return false;
}
domain.split('.').all(|label| {
!label.is_empty()
&& label.len() <= MAX_DOMAIN_LABEL_LENGTH
&& !label.starts_with('-')
&& !label.ends_with('-')
&& label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
})
validate_email_detailed(email.trim()).is_ok()
}
pub fn is_valid_telegram_username(username: &str) -> bool {
@@ -22,7 +22,7 @@ pub async fn require_verified_or_delegated<'a>(
user: &'a AuthenticatedUser,
) -> Result<AccountVerified<'a>, ApiError> {
let is_verified = state
.user_repo
.repos.user
.has_verified_comms_channel(&user.did)
.await
.unwrap_or(false);
@@ -32,7 +32,7 @@ pub async fn require_verified_or_delegated<'a>(
}
let is_delegated = state
.delegation_repo
.repos.delegation
.is_delegated_account(&user.did)
.await
.unwrap_or(false);
@@ -45,7 +45,7 @@ pub async fn require_verified_or_delegated<'a>(
}
pub async fn require_not_migrated(state: &AppState, did: &Did) -> Result<(), ApiError> {
match state.user_repo.is_account_migrated(did).await {
match state.repos.user.is_account_migrated(did).await {
Ok(true) => Err(ApiError::AccountMigrated),
Ok(false) => Ok(()),
Err(e) => {
+7 -6
View File
@@ -232,7 +232,7 @@ async fn verify_oauth_token_and_build_user(
uri: &str,
) -> Result<AuthenticatedUser, AuthError> {
match crate::oauth::verify::verify_oauth_access_token(
state.oauth_repo.as_ref(),
state.repos.oauth.as_ref(),
token,
dpop_proof,
method,
@@ -242,7 +242,7 @@ async fn verify_oauth_token_and_build_user(
{
Ok(result) => {
let user_info = state
.user_repo
.repos.user
.get_user_info_by_did(&result.did)
.await
.ok()
@@ -254,9 +254,10 @@ async fn verify_oauth_token_and_build_user(
);
Ok(AuthenticatedUser {
did: result.did,
key_bytes: user_info.key_bytes.and_then(|kb| {
crate::config::decrypt_key(&kb, user_info.encryption_version).ok()
}),
key_bytes: super::try_decrypt_user_key(
user_info.key_bytes.as_deref(),
user_info.encryption_version,
),
is_admin: user_info.is_admin,
status,
scope: result.scope,
@@ -320,7 +321,7 @@ async fn extract_auth_internal(
.unwrap_or_else(|| parts.uri.path().to_string());
let uri = build_full_url(&original_uri);
match validate_bearer_token_for_service_auth(state.user_repo.as_ref(), &extracted.token).await {
match validate_bearer_token_for_service_auth(state.repos.user.as_ref(), &extracted.token).await {
Ok(user) if !user.auth_source.is_oauth() => {
return Ok(ExtractedAuth::User(user));
}
+19 -19
View File
@@ -76,12 +76,12 @@ pub async fn require_legacy_session_mfa<'a>(
) -> Result<MfaVerified<'a>, ApiError> {
use crate::auth::reauth::check_legacy_session_mfa;
if check_legacy_session_mfa(&*state.session_repo, &user.did).await {
if check_legacy_session_mfa(&*state.repos.session, &user.did).await {
Ok(MfaVerified::from_session_reauth(user))
} else {
let methods = crate::auth::reauth::get_available_reauth_methods(
&*state.user_repo,
&*state.session_repo,
&*state.repos.user,
&*state.repos.session,
&user.did,
)
.await;
@@ -99,7 +99,7 @@ pub async fn require_reauth_window<'a>(
use chrono::Utc;
let status = state
.session_repo
.repos.session
.get_session_mfa_status(&user.did)
.await
.ok()
@@ -114,8 +114,8 @@ pub async fn require_reauth_window<'a>(
}
}
let methods = crate::auth::reauth::get_available_reauth_methods(
&*state.user_repo,
&*state.session_repo,
&*state.repos.user,
&*state.repos.session,
&user.did,
)
.await;
@@ -125,8 +125,8 @@ pub async fn require_reauth_window<'a>(
}
None => {
let methods = crate::auth::reauth::get_available_reauth_methods(
&*state.user_repo,
&*state.session_repo,
&*state.repos.user,
&*state.repos.session,
&user.did,
)
.await;
@@ -144,19 +144,19 @@ pub async fn require_reauth_window_if_available<'a>(
use crate::auth::reauth::check_reauth_required_cached;
let has_password = state
.user_repo
.repos.user
.has_password_by_did(&user.did)
.await
.ok()
.flatten()
.unwrap_or(false);
let has_passkeys = state
.user_repo
.repos.user
.has_passkeys(&user.did)
.await
.unwrap_or(false);
let has_totp = state
.user_repo
.repos.user
.has_totp_enabled(&user.did)
.await
.unwrap_or(false);
@@ -167,10 +167,10 @@ pub async fn require_reauth_window_if_available<'a>(
return Ok(None);
}
if check_reauth_required_cached(&*state.session_repo, &state.cache, &user.did).await {
if check_reauth_required_cached(&*state.repos.session, &state.cache, &user.did).await {
let methods = crate::auth::reauth::get_available_reauth_methods(
&*state.user_repo,
&*state.session_repo,
&*state.repos.user,
&*state.repos.session,
&user.did,
)
.await;
@@ -188,7 +188,7 @@ pub async fn verify_password_mfa<'a>(
password: &str,
) -> Result<MfaVerified<'a>, crate::api::error::ApiError> {
let hash = state
.user_repo
.repos.user
.get_password_hash_by_did(&user.did)
.await
.ok()
@@ -220,7 +220,7 @@ pub async fn verify_totp_mfa<'a>(
if is_backup_code_format(code) {
let backup_codes = state
.user_repo
.repos.user
.get_unused_backup_codes(&user.did)
.await
.ok()
@@ -233,7 +233,7 @@ pub async fn verify_totp_mfa<'a>(
return match matched {
Some(row) => {
let _ = state.user_repo.mark_backup_code_used(row.id).await;
let _ = state.repos.user.mark_backup_code_used(row.id).await;
Ok(MfaVerified::from_recovery_code(user))
}
None => Err(crate::api::error::ApiError::InvalidCode(Some(
@@ -242,7 +242,7 @@ pub async fn verify_totp_mfa<'a>(
};
}
let verified_record = match state.user_repo.get_totp_record_state(&user.did).await {
let verified_record = match state.repos.user.get_totp_record_state(&user.did).await {
Ok(Some(TotpRecordState::Verified(record))) => record,
_ => {
return Err(crate::api::error::ApiError::TotpNotEnabled);
@@ -256,7 +256,7 @@ pub async fn verify_totp_mfa<'a>(
.map_err(|_| crate::api::error::ApiError::InternalError(None))?;
if verify_totp_code(&secret, code) {
let _ = state.user_repo.update_totp_last_used(&user.did).await;
let _ = state.repos.user.update_totp_last_used(&user.did).await;
Ok(MfaVerified::from_totp(user))
} else {
Err(crate::api::error::ApiError::InvalidCode(Some(
+18 -14
View File
@@ -61,6 +61,16 @@ pub fn lxm_permits(lxm: &str, expected: &str) -> bool {
lxm == "*" || lxm == expected
}
pub fn try_decrypt_user_key(
key_bytes: Option<&[u8]>,
encryption_version: Option<i32>,
) -> Option<Vec<u8>> {
match (key_bytes, encryption_version) {
(Some(kb), Some(ev)) => crate::config::decrypt_key(kb, Some(ev)).ok(),
_ => None,
}
}
pub fn encrypt_totp_secret(secret: &[u8]) -> Result<Vec<u8>, crate::config::CryptoError> {
crate::config::encrypt_key(secret)
}
@@ -486,13 +496,10 @@ async fn validate_bearer_token_with_options_internal(
let now = chrono::Utc::now();
if oauth_token.expires_at > now {
let key_bytes = if let (Some(kb), Some(ev)) =
(&oauth_token.key_bytes, oauth_token.encryption_version)
{
crate::config::decrypt_key(kb, Some(ev)).ok()
} else {
None
};
let key_bytes = try_decrypt_user_key(
oauth_token.key_bytes.as_deref(),
oauth_token.encryption_version,
);
let did: Did = oauth_token
.did
.parse()
@@ -592,13 +599,10 @@ pub async fn validate_token_with_dpop(
if !allow_takendown && status.is_takendown() {
return Err(TokenValidationError::AccountTakedown);
}
let key_bytes = if let (Some(kb), Some(ev)) =
(&user_info.key_bytes, user_info.encryption_version)
{
crate::config::decrypt_key(kb, Some(ev)).ok()
} else {
None
};
let key_bytes = try_decrypt_user_key(
user_info.key_bytes.as_deref(),
user_info.encryption_version,
);
Ok(AuthenticatedUser {
did: result_did,
key_bytes,
+1 -1
View File
@@ -26,7 +26,7 @@ pub struct ResolvedIdentity {
pub async fn resolve_identity(state: &AppState, did: &Did) -> Option<ResolvedIdentity> {
let is_local = state
.user_repo
.repos.user
.get_by_did(did)
.await
.ok()
+2 -2
View File
@@ -29,9 +29,9 @@ async fn check_delegation_flag(
error_msg: &str,
) -> Result<bool, ApiError> {
let result = if check_is_delegated {
state.delegation_repo.is_delegated_account(did).await
state.repos.delegation.is_delegated_account(did).await
} else {
state.delegation_repo.controls_any_accounts(did).await
state.repos.delegation.controls_any_accounts(did).await
};
match result {
Ok(true) => Err(ApiError::InvalidDelegation(error_msg.into())),
+2 -2
View File
@@ -302,7 +302,7 @@ impl FromRequestParts<AppState> for OAuthUser {
.headers
.get(crate::util::HEADER_DPOP)
.and_then(|v| v.to_str().ok());
if let Ok(result) = try_legacy_auth(state.user_repo.as_ref(), token).await {
if let Ok(result) = try_legacy_auth(state.repos.user.as_ref(), token).await {
return Ok(OAuthUser {
did: result.did,
client_id: None,
@@ -314,7 +314,7 @@ impl FromRequestParts<AppState> for OAuthUser {
let http_method = parts.method.as_str();
let http_uri = crate::util::build_full_url(&parts.uri.to_string());
match verify_oauth_access_token(
state.oauth_repo.as_ref(),
state.repos.oauth.as_ref(),
token,
dpop_proof,
http_method,
+44 -91
View File
@@ -2,16 +2,21 @@ mod extractor;
pub use extractor::*;
use crate::state::RateLimitKind;
use governor::{
Quota, RateLimiter,
RateLimiter,
clock::DefaultClock,
state::{InMemoryState, NotKeyed, keyed::DefaultKeyedStateStore},
};
use std::{num::NonZeroU32, sync::Arc};
use std::sync::Arc;
pub type KeyedRateLimiter = RateLimiter<String, DefaultKeyedStateStore<String>, DefaultClock>;
pub type GlobalRateLimiter = RateLimiter<NotKeyed, InMemoryState, DefaultClock>;
fn keyed_limiter(kind: RateLimitKind) -> Arc<KeyedRateLimiter> {
Arc::new(RateLimiter::keyed(kind.params().to_governor_quota()))
}
#[derive(Clone)]
pub struct RateLimiters {
pub login: Arc<KeyedRateLimiter>,
@@ -45,123 +50,67 @@ impl Default for RateLimiters {
impl RateLimiters {
pub fn new() -> Self {
Self {
login: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(10).unwrap() },
))),
oauth_token: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(300).unwrap() },
))),
oauth_authorize: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(10).unwrap() },
))),
password_reset: Arc::new(RateLimiter::keyed(Quota::per_hour(
const { NonZeroU32::new(5).unwrap() },
))),
account_creation: Arc::new(RateLimiter::keyed(Quota::per_hour(
const { NonZeroU32::new(10).unwrap() },
))),
refresh_session: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(60).unwrap() },
))),
reset_password: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(10).unwrap() },
))),
oauth_par: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(30).unwrap() },
))),
oauth_introspect: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(30).unwrap() },
))),
app_password: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(10).unwrap() },
))),
email_update: Arc::new(RateLimiter::keyed(Quota::per_hour(
const { NonZeroU32::new(5).unwrap() },
))),
totp_verify: Arc::new(RateLimiter::keyed(
Quota::with_period(std::time::Duration::from_secs(60))
.unwrap()
.allow_burst(const { NonZeroU32::new(5).unwrap() }),
)),
handle_update: Arc::new(RateLimiter::keyed(
Quota::with_period(std::time::Duration::from_secs(30))
.unwrap()
.allow_burst(const { NonZeroU32::new(10).unwrap() }),
)),
handle_update_daily: Arc::new(RateLimiter::keyed(
Quota::with_period(std::time::Duration::from_secs(1728))
.unwrap()
.allow_burst(const { NonZeroU32::new(50).unwrap() }),
)),
verification_check: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(60).unwrap() },
))),
sso_initiate: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(10).unwrap() },
))),
sso_callback: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(30).unwrap() },
))),
sso_unlink: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(10).unwrap() },
))),
oauth_register_complete: Arc::new(RateLimiter::keyed(
Quota::with_period(std::time::Duration::from_secs(60))
.unwrap()
.allow_burst(const { NonZeroU32::new(5).unwrap() }),
)),
handle_verification: Arc::new(RateLimiter::keyed(Quota::per_minute(
const { NonZeroU32::new(10).unwrap() },
))),
login: keyed_limiter(RateLimitKind::Login),
oauth_token: keyed_limiter(RateLimitKind::OAuthToken),
oauth_authorize: keyed_limiter(RateLimitKind::OAuthAuthorize),
password_reset: keyed_limiter(RateLimitKind::PasswordReset),
account_creation: keyed_limiter(RateLimitKind::AccountCreation),
refresh_session: keyed_limiter(RateLimitKind::RefreshSession),
reset_password: keyed_limiter(RateLimitKind::ResetPassword),
oauth_par: keyed_limiter(RateLimitKind::OAuthPar),
oauth_introspect: keyed_limiter(RateLimitKind::OAuthIntrospect),
app_password: keyed_limiter(RateLimitKind::AppPassword),
email_update: keyed_limiter(RateLimitKind::EmailUpdate),
totp_verify: keyed_limiter(RateLimitKind::TotpVerify),
handle_update: keyed_limiter(RateLimitKind::HandleUpdate),
handle_update_daily: keyed_limiter(RateLimitKind::HandleUpdateDaily),
verification_check: keyed_limiter(RateLimitKind::VerificationCheck),
sso_initiate: keyed_limiter(RateLimitKind::SsoInitiate),
sso_callback: keyed_limiter(RateLimitKind::SsoCallback),
sso_unlink: keyed_limiter(RateLimitKind::SsoUnlink),
oauth_register_complete: keyed_limiter(RateLimitKind::OAuthRegisterComplete),
handle_verification: keyed_limiter(RateLimitKind::HandleVerification),
}
}
pub fn override_limit(kind: RateLimitKind, limit: u32) -> Arc<KeyedRateLimiter> {
let mut params = kind.params();
params.limit = limit;
Arc::new(RateLimiter::keyed(params.to_governor_quota()))
}
pub fn with_login_limit(mut self, per_minute: u32) -> Self {
self.login = Arc::new(RateLimiter::keyed(Quota::per_minute(
NonZeroU32::new(per_minute).unwrap_or(const { NonZeroU32::new(10).unwrap() }),
)));
self.login = Self::override_limit(RateLimitKind::Login, per_minute);
self
}
pub fn with_oauth_token_limit(mut self, per_minute: u32) -> Self {
self.oauth_token = Arc::new(RateLimiter::keyed(Quota::per_minute(
NonZeroU32::new(per_minute).unwrap_or(const { NonZeroU32::new(30).unwrap() }),
)));
self.oauth_token = Self::override_limit(RateLimitKind::OAuthToken, per_minute);
self
}
pub fn with_oauth_authorize_limit(mut self, per_minute: u32) -> Self {
self.oauth_authorize = Arc::new(RateLimiter::keyed(Quota::per_minute(
NonZeroU32::new(per_minute).unwrap_or(const { NonZeroU32::new(10).unwrap() }),
)));
self.oauth_authorize = Self::override_limit(RateLimitKind::OAuthAuthorize, per_minute);
self
}
pub fn with_password_reset_limit(mut self, per_hour: u32) -> Self {
self.password_reset = Arc::new(RateLimiter::keyed(Quota::per_hour(
NonZeroU32::new(per_hour).unwrap_or(const { NonZeroU32::new(5).unwrap() }),
)));
self.password_reset = Self::override_limit(RateLimitKind::PasswordReset, per_hour);
self
}
pub fn with_account_creation_limit(mut self, per_hour: u32) -> Self {
self.account_creation = Arc::new(RateLimiter::keyed(Quota::per_hour(
NonZeroU32::new(per_hour).unwrap_or(const { NonZeroU32::new(10).unwrap() }),
)));
self.account_creation = Self::override_limit(RateLimitKind::AccountCreation, per_hour);
self
}
pub fn with_email_update_limit(mut self, per_hour: u32) -> Self {
self.email_update = Arc::new(RateLimiter::keyed(Quota::per_hour(
NonZeroU32::new(per_hour).unwrap_or(const { NonZeroU32::new(5).unwrap() }),
)));
self.email_update = Self::override_limit(RateLimitKind::EmailUpdate, per_hour);
self
}
pub fn with_sso_initiate_limit(mut self, per_minute: u32) -> Self {
self.sso_initiate = Arc::new(RateLimiter::keyed(Quota::per_minute(
NonZeroU32::new(per_minute).unwrap_or(const { NonZeroU32::new(10).unwrap() }),
)));
self.sso_initiate = Self::override_limit(RateLimitKind::SsoInitiate, per_minute);
self
}
}
@@ -178,6 +127,8 @@ mod tests {
#[test]
fn test_rate_limiter_exhaustion() {
use governor::Quota;
use std::num::NonZeroU32;
let limiter = RateLimiter::keyed(Quota::per_minute(const { NonZeroU32::new(2).unwrap() }));
let key = "test_ip".to_string();
@@ -188,6 +139,8 @@ mod tests {
#[test]
fn test_different_keys_have_separate_limits() {
use governor::Quota;
use std::num::NonZeroU32;
let limiter = RateLimiter::keyed(Quota::per_minute(const { NonZeroU32::new(1).unwrap() }));
assert!(limiter.check_key(&"ip1".to_string()).is_ok());
-4
View File
@@ -1,5 +1 @@
pub use tranquil_repo::{PostgresBlockStore, TrackingBlockStore};
pub mod tracking {
pub use tranquil_repo::TrackingBlockStore;
}
+16 -35
View File
@@ -1,6 +1,6 @@
use crate::api::error::ApiError;
use crate::cid_types::CommitCid;
use crate::repo::tracking::TrackingBlockStore;
use crate::repo::TrackingBlockStore;
use crate::state::AppState;
use crate::types::{Did, Handle, Nsid, Rkey};
use bytes::Bytes;
@@ -85,7 +85,7 @@ impl From<CommitError> for ApiError {
pub async fn get_current_root_cid(state: &AppState, user_id: Uuid) -> Result<CommitCid, ApiError> {
let root_cid_str = state
.repo_repo
.repos.repo
.get_repo_root_cid_by_user_id(user_id)
.await
.map_err(|e| {
@@ -98,29 +98,10 @@ pub async fn get_current_root_cid(state: &AppState, user_id: Uuid) -> Result<Com
}
pub fn extract_blob_cids(record: &Value) -> Vec<String> {
let mut blobs = Vec::new();
extract_blob_cids_recursive(record, &mut blobs);
blobs
}
fn extract_blob_cids_recursive(value: &Value, blobs: &mut Vec<String>) {
match value {
Value::Object(map) => {
if map.get("$type").and_then(|v| v.as_str()) == Some("blob")
&& let Some(ref_obj) = map.get("ref")
&& let Some(link) = ref_obj.get("$link").and_then(|v| v.as_str())
{
blobs.push(link.to_string());
}
map.values()
.for_each(|v| extract_blob_cids_recursive(v, blobs));
}
Value::Array(arr) => {
arr.iter()
.for_each(|v| extract_blob_cids_recursive(v, blobs));
}
_ => {}
}
crate::sync::import::find_blob_refs(record, 0)
.into_iter()
.map(|b| b.cid)
.collect()
}
use crate::types::AtUri;
@@ -187,7 +168,7 @@ pub async fn begin_repo_write(
let write_lock = state.repo_write_locks.lock(user_id).await;
let root_cid_str = state
.repo_repo
.repos.repo
.get_repo_root_cid_by_user_id(user_id)
.await
.map_err(|e| {
@@ -273,7 +254,7 @@ pub async fn finalize_repo_write(
if let Some(controller_did) = params.controller_did
&& let Some(detail) = params.delegation_detail
&& let Err(e) = state
.delegation_repo
.repos.delegation
.log_delegation_action(
params.did,
controller_did,
@@ -370,7 +351,7 @@ pub async fn commit_and_log(
obsolete_cids,
} = params;
let key_row = state
.user_repo
.repos.user
.get_user_key_by_id(user_id)
.await
.map_err(|e| CommitError::DatabaseError(format!("Failed to fetch signing key: {}", e)))?
@@ -504,7 +485,7 @@ pub async fn commit_and_log(
};
let _result = state
.repo_repo
.repos.repo
.apply_commit(input)
.await
.map_err(|e| match e {
@@ -526,7 +507,7 @@ pub async fn create_record_internal(
record: &serde_json::Value,
) -> Result<(String, Cid), CommitError> {
let user_id: Uuid = state
.user_repo
.repos.user
.get_id_by_did(did)
.await
.map_err(|e| CommitError::DatabaseError(e.to_string()))?
@@ -535,7 +516,7 @@ pub async fn create_record_internal(
let _write_lock = state.repo_write_locks.lock(user_id).await;
let root_cid_link = state
.repo_repo
.repos.repo
.get_repo_root_cid_by_user_id(user_id)
.await
.map_err(|e| CommitError::DatabaseError(e.to_string()))?
@@ -629,7 +610,7 @@ pub async fn sequence_identity_event(
handle: Option<&Handle>,
) -> Result<SequenceNumber, CommitError> {
state
.repo_repo
.repos.repo
.insert_identity_event(did, handle)
.await
.map_err(|e| CommitError::DatabaseError(format!("identity event: {}", e)))
@@ -640,7 +621,7 @@ pub async fn sequence_account_event(
status: tranquil_db_traits::AccountStatus,
) -> Result<SequenceNumber, CommitError> {
state
.repo_repo
.repos.repo
.insert_account_event(did, status)
.await
.map_err(|e| CommitError::DatabaseError(format!("account event: {}", e)))
@@ -655,7 +636,7 @@ pub async fn sequence_sync_event(
.parse()
.map_err(|_| CommitError::InvalidCid(commit_cid.to_string()))?;
state
.repo_repo
.repos.repo
.insert_sync_event(did, &cid_link, rev)
.await
.map_err(|e| CommitError::DatabaseError(format!("sync event: {}", e)))
@@ -671,7 +652,7 @@ pub async fn sequence_genesis_commit(
let commit_cid_link = crate::types::CidLink::from(commit_cid);
let mst_root_cid_link = crate::types::CidLink::from(mst_root_cid);
state
.repo_repo
.repos.repo
.insert_genesis_commit_event(did, &commit_cid_link, &mst_root_cid_link, rev)
.await
.map_err(|e| CommitError::DatabaseError(format!("genesis commit event: {}", e)))
+13 -26
View File
@@ -16,11 +16,7 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
use tranquil_db::{
BacklinkRepository, BlobRepository, DelegationRepository, InfraRepository, OAuthRepository,
PostgresRepositories, RepoEventNotifier, RepoRepository, SessionRepository, SsoRepository,
UserRepository,
};
use tranquil_db::PostgresRepositories;
use tranquil_db_traits::SequencedEvent;
static RATE_LIMITING_DISABLED: AtomicBool = AtomicBool::new(false);
@@ -36,15 +32,6 @@ pub fn init_rate_limit_override() {
#[derive(Clone)]
pub struct AppState {
pub repos: Arc<PostgresRepositories>,
pub user_repo: Arc<dyn UserRepository>,
pub oauth_repo: Arc<dyn OAuthRepository>,
pub session_repo: Arc<dyn SessionRepository>,
pub delegation_repo: Arc<dyn DelegationRepository>,
pub repo_repo: Arc<dyn RepoRepository>,
pub blob_repo: Arc<dyn BlobRepository>,
pub infra_repo: Arc<dyn InfraRepository>,
pub backlink_repo: Arc<dyn BacklinkRepository>,
pub event_notifier: Arc<dyn RepoEventNotifier>,
pub block_store: PostgresBlockStore,
pub blob_store: Arc<dyn BlobStorage>,
pub firehose_tx: broadcast::Sender<SequencedEvent>,
@@ -54,7 +41,6 @@ pub struct AppState {
pub cache: Arc<dyn Cache>,
pub distributed_rate_limiter: Arc<dyn DistributedRateLimiter>,
pub did_resolver: Arc<DidResolver>,
pub sso_repo: Arc<dyn SsoRepository>,
pub sso_manager: SsoManager,
pub webauthn_config: Arc<WebAuthnConfig>,
pub cross_pds_oauth: Arc<CrossPdsOAuthClient>,
@@ -69,6 +55,17 @@ pub struct RateLimitParams {
pub window_ms: u64,
}
impl RateLimitParams {
pub fn to_governor_quota(self) -> governor::Quota {
use std::num::NonZeroU32;
let burst = NonZeroU32::new(self.limit).unwrap_or(NonZeroU32::MIN);
let period = std::time::Duration::from_millis(self.window_ms);
governor::Quota::with_period(period)
.expect("rate limit window must be non-zero")
.allow_burst(burst)
}
}
#[derive(Debug, Clone, Copy)]
pub enum RateLimitKind {
Login,
@@ -119,7 +116,7 @@ impl RateLimitKind {
}
}
const fn params(&self) -> RateLimitParams {
pub const fn params(&self) -> RateLimitParams {
match self {
Self::Login => RateLimitParams {
limit: 10,
@@ -286,16 +283,6 @@ impl AppState {
);
Self {
user_repo: repos.user.clone(),
oauth_repo: repos.oauth.clone(),
session_repo: repos.session.clone(),
delegation_repo: repos.delegation.clone(),
repo_repo: repos.repo.clone(),
blob_repo: repos.blob.clone(),
infra_repo: repos.infra.clone(),
backlink_repo: repos.backlink.clone(),
event_notifier: repos.event_notifier.clone(),
sso_repo: repos.sso.clone(),
repos,
block_store,
blob_store,
+6 -22
View File
@@ -25,21 +25,14 @@ static DISCORD_APP_ID: OnceLock<String> = OnceLock::new();
static TELEGRAM_BOT_USERNAME: OnceLock<String> = OnceLock::new();
pub fn generate_token_code() -> String {
generate_token_code_parts(2, 5)
}
pub fn generate_token_code_parts(parts: usize, part_len: usize) -> String {
let mut rng = rand::thread_rng();
let chars: Vec<char> = BASE32_ALPHABET.chars().collect();
(0..parts)
.map(|_| {
(0..part_len)
.map(|_| chars[rng.gen_range(0..chars.len())])
.collect::<String>()
})
.collect::<Vec<_>>()
.join("-")
let gen_segment = |rng: &mut rand::rngs::ThreadRng| -> String {
(0..5)
.map(|_| chars[rng.gen_range(0..chars.len())])
.collect()
};
format!("{}-{}", gen_segment(&mut rng), gen_segment(&mut rng))
}
pub fn parse_repeated_query_param(query: Option<&str>, key: &str) -> Vec<String> {
@@ -295,15 +288,6 @@ mod tests {
);
}
#[test]
fn test_generate_token_code_parts() {
let code = generate_token_code_parts(3, 4);
let parts: Vec<&str> = code.split('-').collect();
assert_eq!(parts.len(), 3);
assert!(parts.iter().all(|part| part.len() == 4));
}
#[test]
fn test_json_to_ipld_cid_link() {
let json = serde_json::json!({