diff --git a/.config/nextest.toml b/.config/nextest.toml index 2cfd181..6aa4e79 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,10 +1,23 @@ [store] dir = "target/nextest" + [profile.default] retries = 0 fail-fast = true test-threads = "num-cpus" + [profile.ci] retries = 2 fail-fast = false test-threads = "num-cpus" + +[test-groups] +serial-env-tests = { max-threads = 1 } + +[[profile.default.overrides]] +filter = "test(/import_with_verification/) | test(/plc_migration/)" +test-group = "serial-env-tests" + +[[profile.ci.overrides]] +filter = "test(/import_with_verification/) | test(/plc_migration/)" +test-group = "serial-env-tests" diff --git a/.sqlx/query-0236504c0d6096dcdab0d5143737ef762de989fb560247e2540f611e28362507.json b/.sqlx/query-0236504c0d6096dcdab0d5143737ef762de989fb560247e2540f611e28362507.json new file mode 100644 index 0000000..521625c --- /dev/null +++ b/.sqlx/query-0236504c0d6096dcdab0d5143737ef762de989fb560247e2540f611e28362507.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE oauth_device SET trusted_until = $1 WHERE id = $2 AND trusted_until IS NOT NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Text" + ] + }, + "nullable": [] + }, + "hash": "0236504c0d6096dcdab0d5143737ef762de989fb560247e2540f611e28362507" +} diff --git a/.sqlx/query-0819ec49e0f3b97eb7eff0a1ef0bbc6683442938b657e82da283bc07ceec4c80.json b/.sqlx/query-0819ec49e0f3b97eb7eff0a1ef0bbc6683442938b657e82da283bc07ceec4c80.json new file mode 100644 index 0000000..0cac9aa --- /dev/null +++ b/.sqlx/query-0819ec49e0f3b97eb7eff0a1ef0bbc6683442938b657e82da283bc07ceec4c80.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE oauth_device SET trusted_at = NULL, trusted_until = NULL WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "0819ec49e0f3b97eb7eff0a1ef0bbc6683442938b657e82da283bc07ceec4c80" +} diff --git a/.sqlx/query-08c08b0644d79d5de72f3500dd7dbb8827af340e3c04fec9a5c28aeff46e0c97.json b/.sqlx/query-08c08b0644d79d5de72f3500dd7dbb8827af340e3c04fec9a5c28aeff46e0c97.json index 8b18439..c210e37 100644 --- a/.sqlx/query-08c08b0644d79d5de72f3500dd7dbb8827af340e3c04fec9a5c28aeff46e0c97.json +++ b/.sqlx/query-08c08b0644d79d5de72f3500dd7dbb8827af340e3c04fec9a5c28aeff46e0c97.json @@ -26,7 +26,7 @@ }, "nullable": [ false, - false, + true, false ] }, diff --git a/.sqlx/query-17dfafc85b3434ed78041f48809580a02c92e579869f647cb08f65ac777854f5.json b/.sqlx/query-17dfafc85b3434ed78041f48809580a02c92e579869f647cb08f65ac777854f5.json index 4444b64..10b2ebf 100644 --- a/.sqlx/query-17dfafc85b3434ed78041f48809580a02c92e579869f647cb08f65ac777854f5.json +++ b/.sqlx/query-17dfafc85b3434ed78041f48809580a02c92e579869f647cb08f65ac777854f5.json @@ -38,7 +38,8 @@ "admin_email", "plc_operation", "two_factor_code", - "channel_verification" + "channel_verification", + "passkey_recovery" ] } } diff --git a/.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json b/.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json index 7004e69..22f63d9 100644 --- a/.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json +++ b/.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json @@ -46,7 +46,8 @@ "admin_email", "plc_operation", "two_factor_code", - "channel_verification" + "channel_verification", + "passkey_recovery" ] } } diff --git a/.sqlx/query-2e1d13f0b6fb1dc5021740674fab3776851008324d64e0fdf04677105d0189d2.json b/.sqlx/query-2e1d13f0b6fb1dc5021740674fab3776851008324d64e0fdf04677105d0189d2.json new file mode 100644 index 0000000..60cc9ec --- /dev/null +++ b/.sqlx/query-2e1d13f0b6fb1dc5021740674fab3776851008324d64e0fdf04677105d0189d2.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT ap.password_hash FROM app_passwords ap\n JOIN users u ON ap.user_id = u.id\n WHERE u.did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "password_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "2e1d13f0b6fb1dc5021740674fab3776851008324d64e0fdf04677105d0189d2" +} diff --git a/.sqlx/query-33f1362cf0836f642ebf6fc053ee92ffef44ef4b67ddc00327c6cd407b3436b8.json b/.sqlx/query-33f1362cf0836f642ebf6fc053ee92ffef44ef4b67ddc00327c6cd407b3436b8.json new file mode 100644 index 0000000..a7770f3 --- /dev/null +++ b/.sqlx/query-33f1362cf0836f642ebf6fc053ee92ffef44ef4b67ddc00327c6cd407b3436b8.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "33f1362cf0836f642ebf6fc053ee92ffef44ef4b67ddc00327c6cd407b3436b8" +} diff --git a/.sqlx/query-3519df39bff89306f6a8f38709d4705adf34732730dab8346f814d8ef7599a74.json b/.sqlx/query-3519df39bff89306f6a8f38709d4705adf34732730dab8346f814d8ef7599a74.json new file mode 100644 index 0000000..a9ac379 --- /dev/null +++ b/.sqlx/query-3519df39bff89306f6a8f38709d4705adf34732730dab8346f814d8ef7599a74.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE oauth_device SET trusted_at = $1, trusted_until = $2 WHERE id = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Timestamptz", + "Text" + ] + }, + "nullable": [] + }, + "hash": "3519df39bff89306f6a8f38709d4705adf34732730dab8346f814d8ef7599a74" +} diff --git a/.sqlx/query-39fc4114472fd390ea5921c27622a1aeb1ea927d85e0d90392e25bfa440d364d.json b/.sqlx/query-39fc4114472fd390ea5921c27622a1aeb1ea927d85e0d90392e25bfa440d364d.json new file mode 100644 index 0000000..04da0a9 --- /dev/null +++ b/.sqlx/query-39fc4114472fd390ea5921c27622a1aeb1ea927d85e0d90392e25bfa440d364d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE oauth_device SET friendly_name = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "39fc4114472fd390ea5921c27622a1aeb1ea927d85e0d90392e25bfa440d364d" +} diff --git a/.sqlx/query-3f9b3b06f54df7c1d20ea9ff94b914ad3bf77d47dd393a0aae1c030b8ce98bcc.json b/.sqlx/query-3f9b3b06f54df7c1d20ea9ff94b914ad3bf77d47dd393a0aae1c030b8ce98bcc.json index d87a8a1..b1ef31f 100644 --- a/.sqlx/query-3f9b3b06f54df7c1d20ea9ff94b914ad3bf77d47dd393a0aae1c030b8ce98bcc.json +++ b/.sqlx/query-3f9b3b06f54df7c1d20ea9ff94b914ad3bf77d47dd393a0aae1c030b8ce98bcc.json @@ -38,7 +38,8 @@ "admin_email", "plc_operation", "two_factor_code", - "channel_verification" + "channel_verification", + "passkey_recovery" ] } } diff --git a/.sqlx/query-4513affeac5d8f9b93be23bef92e88b6949869e7d2cd3b40125597e29d7e0d20.json b/.sqlx/query-4513affeac5d8f9b93be23bef92e88b6949869e7d2cd3b40125597e29d7e0d20.json deleted file mode 100644 index 53ad312..0000000 --- a/.sqlx/query-4513affeac5d8f9b93be23bef92e88b6949869e7d2cd3b40125597e29d7e0d20.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at)\n VALUES ($1, 'email', $2, $3, $4)\n ON CONFLICT (user_id, channel) DO UPDATE\n SET code = $2, pending_identifier = $3, expires_at = $4, created_at = NOW()\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Timestamptz" - ] - }, - "nullable": [] - }, - "hash": "4513affeac5d8f9b93be23bef92e88b6949869e7d2cd3b40125597e29d7e0d20" -} diff --git a/.sqlx/query-4d8189361d1da271e300041599561ac07a02ffa9a926f94508d7873c4ca07e65.json b/.sqlx/query-4d8189361d1da271e300041599561ac07a02ffa9a926f94508d7873c4ca07e65.json new file mode 100644 index 0000000..6754294 --- /dev/null +++ b/.sqlx/query-4d8189361d1da271e300041599561ac07a02ffa9a926f94508d7873c4ca07e65.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT trusted_until FROM oauth_device od\n JOIN oauth_account_device oad ON od.id = oad.device_id\n WHERE od.id = $1 AND oad.did = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "trusted_until", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "4d8189361d1da271e300041599561ac07a02ffa9a926f94508d7873c4ca07e65" +} diff --git a/.sqlx/query-4f411f02bc10c6961a7134c7f1c2446a677d8ceb49ea00542f164dbb508f205f.json b/.sqlx/query-4f411f02bc10c6961a7134c7f1c2446a677d8ceb49ea00542f164dbb508f205f.json new file mode 100644 index 0000000..ba6da25 --- /dev/null +++ b/.sqlx/query-4f411f02bc10c6961a7134c7f1c2446a677d8ceb49ea00542f164dbb508f205f.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO app_passwords (user_id, name, password_hash, privileged) VALUES ($1, $2, $3, FALSE)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "4f411f02bc10c6961a7134c7f1c2446a677d8ceb49ea00542f164dbb508f205f" +} diff --git a/.sqlx/query-54ec6149f129881362891151da8200baef1f16427d87fb3afeb1e066c4084483.json b/.sqlx/query-54ec6149f129881362891151da8200baef1f16427d87fb3afeb1e066c4084483.json new file mode 100644 index 0000000..e3a0c80 --- /dev/null +++ b/.sqlx/query-54ec6149f129881362891151da8200baef1f16427d87fb3afeb1e066c4084483.json @@ -0,0 +1,30 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at) VALUES ($1, $2::comms_channel, $3, $4, $5)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + { + "Custom": { + "name": "comms_channel", + "kind": { + "Enum": [ + "email", + "discord", + "telegram", + "signal" + ] + } + } + }, + "Text", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "54ec6149f129881362891151da8200baef1f16427d87fb3afeb1e066c4084483" +} diff --git a/.sqlx/query-5934c4b41c2334a08742ee80d91b2355892675be8cd589636d94f11d0f730bbc.json b/.sqlx/query-5934c4b41c2334a08742ee80d91b2355892675be8cd589636d94f11d0f730bbc.json new file mode 100644 index 0000000..6cd55ff --- /dev/null +++ b/.sqlx/query-5934c4b41c2334a08742ee80d91b2355892675be8cd589636d94f11d0f730bbc.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT available_uses > 0 AND NOT disabled FROM invite_codes WHERE code = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "5934c4b41c2334a08742ee80d91b2355892675be8cd589636d94f11d0f730bbc" +} diff --git a/.sqlx/query-126519f77d91aa1877b2c933a876c0283f9dc49444d68eca4e87461b82f9be32.json b/.sqlx/query-5a3f588a937a44a4e14570a6c13bc6f4c5a2a50155f6e8bdd14beef66dca97c1.json similarity index 50% rename from .sqlx/query-126519f77d91aa1877b2c933a876c0283f9dc49444d68eca4e87461b82f9be32.json rename to .sqlx/query-5a3f588a937a44a4e14570a6c13bc6f4c5a2a50155f6e8bdd14beef66dca97c1.json index aaaa50b..f597186 100644 --- a/.sqlx/query-126519f77d91aa1877b2c933a876c0283f9dc49444d68eca4e87461b82f9be32.json +++ b/.sqlx/query-5a3f588a937a44a4e14570a6c13bc6f4c5a2a50155f6e8bdd14beef66dca97c1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT code, expires_at FROM channel_verifications WHERE user_id = $1 AND channel = 'email'", + "query": "SELECT code, expires_at FROM channel_verifications WHERE user_id = $1 AND channel = $2::comms_channel", "describe": { "columns": [ { @@ -16,7 +16,20 @@ ], "parameters": { "Left": [ - "Uuid" + "Uuid", + { + "Custom": { + "name": "comms_channel", + "kind": { + "Enum": [ + "email", + "discord", + "telegram", + "signal" + ] + } + } + } ] }, "nullable": [ @@ -24,5 +37,5 @@ false ] }, - "hash": "126519f77d91aa1877b2c933a876c0283f9dc49444d68eca4e87461b82f9be32" + "hash": "5a3f588a937a44a4e14570a6c13bc6f4c5a2a50155f6e8bdd14beef66dca97c1" } diff --git a/.sqlx/query-5a7b98295457e43facb537845ed966b4ac507646c442881d0a7aec58725622ed.json b/.sqlx/query-5a7b98295457e43facb537845ed966b4ac507646c442881d0a7aec58725622ed.json new file mode 100644 index 0000000..d1e2982 --- /dev/null +++ b/.sqlx/query-5a7b98295457e43facb537845ed966b4ac507646c442881d0a7aec58725622ed.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT password_hash IS NOT NULL as has_password FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "has_password", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "5a7b98295457e43facb537845ed966b4ac507646c442881d0a7aec58725622ed" +} diff --git a/.sqlx/query-63ccfb04db47b69abf176baedc7b27a1dddea591429b4696dc68105b435b38f3.json b/.sqlx/query-63ccfb04db47b69abf176baedc7b27a1dddea591429b4696dc68105b435b38f3.json new file mode 100644 index 0000000..9258673 --- /dev/null +++ b/.sqlx/query-63ccfb04db47b69abf176baedc7b27a1dddea591429b4696dc68105b435b38f3.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 as one FROM oauth_device od\n JOIN oauth_account_device oad ON od.id = oad.device_id\n WHERE oad.did = $1 AND od.id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "one", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "63ccfb04db47b69abf176baedc7b27a1dddea591429b4696dc68105b435b38f3" +} diff --git a/.sqlx/query-70be96c8f398a75e8d52e07c1d1f80354bbe2f53f494e8e072ef92ef1418b034.json b/.sqlx/query-70be96c8f398a75e8d52e07c1d1f80354bbe2f53f494e8e072ef92ef1418b034.json new file mode 100644 index 0000000..3b32eaa --- /dev/null +++ b/.sqlx/query-70be96c8f398a75e8d52e07c1d1f80354bbe2f53f494e8e072ef92ef1418b034.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT 1 as one FROM app_passwords ap JOIN users u ON ap.user_id = u.id WHERE u.did = $1 LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "one", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "70be96c8f398a75e8d52e07c1d1f80354bbe2f53f494e8e072ef92ef1418b034" +} diff --git a/.sqlx/query-736bd3e5b03b98587e9b611304c55fe004e15020069a53019208deb2ba5be369.json b/.sqlx/query-736bd3e5b03b98587e9b611304c55fe004e15020069a53019208deb2ba5be369.json new file mode 100644 index 0000000..1fb61d4 --- /dev/null +++ b/.sqlx/query-736bd3e5b03b98587e9b611304c55fe004e15020069a53019208deb2ba5be369.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET recovery_token = $1, recovery_token_expires_at = $2 WHERE did = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Timestamptz", + "Text" + ] + }, + "nullable": [] + }, + "hash": "736bd3e5b03b98587e9b611304c55fe004e15020069a53019208deb2ba5be369" +} diff --git a/.sqlx/query-73c166c20b87f199d384d4a03fb7e3f3ea071ffafbeeca821238bc062375953b.json b/.sqlx/query-73c166c20b87f199d384d4a03fb7e3f3ea071ffafbeeca821238bc062375953b.json new file mode 100644 index 0000000..bfaf2d4 --- /dev/null +++ b/.sqlx/query-73c166c20b87f199d384d4a03fb7e3f3ea071ffafbeeca821238bc062375953b.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT handle, recovery_token, recovery_token_expires_at, password_required\n FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "handle", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "recovery_token", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "recovery_token_expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "password_required", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + true, + true, + false + ] + }, + "hash": "73c166c20b87f199d384d4a03fb7e3f3ea071ffafbeeca821238bc062375953b" +} diff --git a/.sqlx/query-76c6ef1d5395105a0cdedb27ca321c9e3eae1ce87c223b706ed81ebf973875f3.json b/.sqlx/query-76c6ef1d5395105a0cdedb27ca321c9e3eae1ce87c223b706ed81ebf973875f3.json new file mode 100644 index 0000000..bedeb59 --- /dev/null +++ b/.sqlx/query-76c6ef1d5395105a0cdedb27ca321c9e3eae1ce87c223b706ed81ebf973875f3.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, password_hash FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "password_hash", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + true + ] + }, + "hash": "76c6ef1d5395105a0cdedb27ca321c9e3eae1ce87c223b706ed81ebf973875f3" +} diff --git a/.sqlx/query-76ff03b78f9a5a7d9b28b9de208b225aeaa1a1ab1f000ab6ca16f5db1ec76180.json b/.sqlx/query-76ff03b78f9a5a7d9b28b9de208b225aeaa1a1ab1f000ab6ca16f5db1ec76180.json new file mode 100644 index 0000000..b99e974 --- /dev/null +++ b/.sqlx/query-76ff03b78f9a5a7d9b28b9de208b225aeaa1a1ab1f000ab6ca16f5db1ec76180.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM passkeys WHERE did = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "76ff03b78f9a5a7d9b28b9de208b225aeaa1a1ab1f000ab6ca16f5db1ec76180" +} diff --git a/.sqlx/query-d2a6047b9f8039025b19028b8db7935ea60bfff1698488cbaacc8785c85c94b4.json b/.sqlx/query-7ac7deac86fece536c0dcc0d3555c3caae31316887a629866c6a90ddee373317.json similarity index 61% rename from .sqlx/query-d2a6047b9f8039025b19028b8db7935ea60bfff1698488cbaacc8785c85c94b4.json rename to .sqlx/query-7ac7deac86fece536c0dcc0d3555c3caae31316887a629866c6a90ddee373317.json index 1cf5faf..e025c98 100644 --- a/.sqlx/query-d2a6047b9f8039025b19028b8db7935ea60bfff1698488cbaacc8785c85c94b4.json +++ b/.sqlx/query-7ac7deac86fece536c0dcc0d3555c3caae31316887a629866c6a90ddee373317.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id FROM users WHERE LOWER(email) = $1", + "query": "SELECT id FROM users WHERE LOWER(email) = $1 OR handle = $2", "describe": { "columns": [ { @@ -11,6 +11,7 @@ ], "parameters": { "Left": [ + "Text", "Text" ] }, @@ -18,5 +19,5 @@ false ] }, - "hash": "d2a6047b9f8039025b19028b8db7935ea60bfff1698488cbaacc8785c85c94b4" + "hash": "7ac7deac86fece536c0dcc0d3555c3caae31316887a629866c6a90ddee373317" } diff --git a/.sqlx/query-8290c8ec5798a827bab64a17c3d4bf34bd0b88971b0658d191ed57badbbfd979.json b/.sqlx/query-8290c8ec5798a827bab64a17c3d4bf34bd0b88971b0658d191ed57badbbfd979.json new file mode 100644 index 0000000..e775ef3 --- /dev/null +++ b/.sqlx/query-8290c8ec5798a827bab64a17c3d4bf34bd0b88971b0658d191ed57badbbfd979.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE session_tokens SET last_reauth_at = $1 WHERE did = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Text" + ] + }, + "nullable": [] + }, + "hash": "8290c8ec5798a827bab64a17c3d4bf34bd0b88971b0658d191ed57badbbfd979" +} diff --git a/.sqlx/query-8835e3653c1b65874ff2828a1993b4505d5442b12d00b9062ee0db5f58ae05b8.json b/.sqlx/query-8835e3653c1b65874ff2828a1993b4505d5442b12d00b9062ee0db5f58ae05b8.json new file mode 100644 index 0000000..b1e7a0b --- /dev/null +++ b/.sqlx/query-8835e3653c1b65874ff2828a1993b4505d5442b12d00b9062ee0db5f58ae05b8.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, did, handle, password_required FROM users WHERE LOWER(email) = $1 OR handle = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "did", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "handle", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "password_required", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "8835e3653c1b65874ff2828a1993b4505d5442b12d00b9062ee0db5f58ae05b8" +} diff --git a/.sqlx/query-976847b83e599effda5ad3c0059cccf1df977c95dba43937de548b56ccc8256a.json b/.sqlx/query-976847b83e599effda5ad3c0059cccf1df977c95dba43937de548b56ccc8256a.json new file mode 100644 index 0000000..93994b9 --- /dev/null +++ b/.sqlx/query-976847b83e599effda5ad3c0059cccf1df977c95dba43937de548b56ccc8256a.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT last_reauth_at FROM session_tokens WHERE did = $1 ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "last_reauth_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "976847b83e599effda5ad3c0059cccf1df977c95dba43937de548b56ccc8256a" +} diff --git a/.sqlx/query-97b7414f11c3d696afe2d7007dbf52074bfda921bbb300f23bdf1ccb096b5ea5.json b/.sqlx/query-97b7414f11c3d696afe2d7007dbf52074bfda921bbb300f23bdf1ccb096b5ea5.json deleted file mode 100644 index 2ae0005..0000000 --- a/.sqlx/query-97b7414f11c3d696afe2d7007dbf52074bfda921bbb300f23bdf1ccb096b5ea5.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at) VALUES ($1, 'email', $2, $3, $4)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Timestamptz" - ] - }, - "nullable": [] - }, - "hash": "97b7414f11c3d696afe2d7007dbf52074bfda921bbb300f23bdf1ccb096b5ea5" -} diff --git a/.sqlx/query-9fc6b64243ef6a4906ea9eb1ae630004dc6b40b9495fa998caf6e4cdd26a43e4.json b/.sqlx/query-9fc6b64243ef6a4906ea9eb1ae630004dc6b40b9495fa998caf6e4cdd26a43e4.json new file mode 100644 index 0000000..c9788a4 --- /dev/null +++ b/.sqlx/query-9fc6b64243ef6a4906ea9eb1ae630004dc6b40b9495fa998caf6e4cdd26a43e4.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET password_hash = NULL, password_required = FALSE WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "9fc6b64243ef6a4906ea9eb1ae630004dc6b40b9495fa998caf6e4cdd26a43e4" +} diff --git a/.sqlx/query-a10a29aee170a54af2ddbd59cf989a2910508b9f7e6f60465dd4cb5c7a79d848.json b/.sqlx/query-a10a29aee170a54af2ddbd59cf989a2910508b9f7e6f60465dd4cb5c7a79d848.json new file mode 100644 index 0000000..b9a0999 --- /dev/null +++ b/.sqlx/query-a10a29aee170a54af2ddbd59cf989a2910508b9f7e6f60465dd4cb5c7a79d848.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, did, recovery_token, recovery_token_expires_at FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "did", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "recovery_token", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "recovery_token_expires_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true, + true + ] + }, + "hash": "a10a29aee170a54af2ddbd59cf989a2910508b9f7e6f60465dd4cb5c7a79d848" +} diff --git a/.sqlx/query-c60e77678da0c42399179015971f55f4f811a0d666237a93035cfece07445590.json b/.sqlx/query-c60e77678da0c42399179015971f55f4f811a0d666237a93035cfece07445590.json index b290de5..a84c501 100644 --- a/.sqlx/query-c60e77678da0c42399179015971f55f4f811a0d666237a93035cfece07445590.json +++ b/.sqlx/query-c60e77678da0c42399179015971f55f4f811a0d666237a93035cfece07445590.json @@ -63,7 +63,7 @@ false, false, false, - false, + true, false, false, false, diff --git a/.sqlx/query-cbd7ee75bb7e318ba7327136094d58397bbf306c249bffd286457e471c00b745.json b/.sqlx/query-cbd7ee75bb7e318ba7327136094d58397bbf306c249bffd286457e471c00b745.json index ab84790..2e3fb27 100644 --- a/.sqlx/query-cbd7ee75bb7e318ba7327136094d58397bbf306c249bffd286457e471c00b745.json +++ b/.sqlx/query-cbd7ee75bb7e318ba7327136094d58397bbf306c249bffd286457e471c00b745.json @@ -15,7 +15,7 @@ ] }, "nullable": [ - false + true ] }, "hash": "cbd7ee75bb7e318ba7327136094d58397bbf306c249bffd286457e471c00b745" diff --git a/.sqlx/query-d77baba1d885d532a18a0376a95774681fb0fe9e88733fa4315e9aef799cd19f.json b/.sqlx/query-d77baba1d885d532a18a0376a95774681fb0fe9e88733fa4315e9aef799cd19f.json new file mode 100644 index 0000000..03a3984 --- /dev/null +++ b/.sqlx/query-d77baba1d885d532a18a0376a95774681fb0fe9e88733fa4315e9aef799cd19f.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT od.id, od.user_agent, od.friendly_name, od.trusted_at, od.trusted_until, od.last_seen_at\n FROM oauth_device od\n JOIN oauth_account_device oad ON od.id = oad.device_id\n WHERE oad.did = $1 AND od.trusted_until IS NOT NULL AND od.trusted_until > NOW()\n ORDER BY od.last_seen_at DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "user_agent", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "friendly_name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "trusted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "trusted_until", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "last_seen_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + true, + true, + true, + true, + false + ] + }, + "hash": "d77baba1d885d532a18a0376a95774681fb0fe9e88733fa4315e9aef799cd19f" +} diff --git a/.sqlx/query-d9409c8faeef28bc048ab4462681a7e3b62280bb697a81cbd39ff8a1207651a5.json b/.sqlx/query-d9409c8faeef28bc048ab4462681a7e3b62280bb697a81cbd39ff8a1207651a5.json new file mode 100644 index 0000000..f2fa3a7 --- /dev/null +++ b/.sqlx/query-d9409c8faeef28bc048ab4462681a7e3b62280bb697a81cbd39ff8a1207651a5.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE users SET password_hash = $1, password_required = TRUE, recovery_token = NULL, recovery_token_expires_at = NULL WHERE did = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "d9409c8faeef28bc048ab4462681a7e3b62280bb697a81cbd39ff8a1207651a5" +} diff --git a/.sqlx/query-d7259198aa28f202fbc5bb9466c8a16446b664532e1bc9eff6a783652265229b.json b/.sqlx/query-e44b36de8d7822040dfaf7407b2ef3787606f9c74041deaceb7b011680f7b0a7.json similarity index 58% rename from .sqlx/query-d7259198aa28f202fbc5bb9466c8a16446b664532e1bc9eff6a783652265229b.json rename to .sqlx/query-e44b36de8d7822040dfaf7407b2ef3787606f9c74041deaceb7b011680f7b0a7.json index 1f3560c..58a4fdd 100644 --- a/.sqlx/query-d7259198aa28f202fbc5bb9466c8a16446b664532e1bc9eff6a783652265229b.json +++ b/.sqlx/query-e44b36de8d7822040dfaf7407b2ef3787606f9c74041deaceb7b011680f7b0a7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2", + "query": "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL, password_required = TRUE WHERE id = $2", "describe": { "columns": [], "parameters": { @@ -11,5 +11,5 @@ }, "nullable": [] }, - "hash": "d7259198aa28f202fbc5bb9466c8a16446b664532e1bc9eff6a783652265229b" + "hash": "e44b36de8d7822040dfaf7407b2ef3787606f9c74041deaceb7b011680f7b0a7" } diff --git a/.sqlx/query-f6aede22ec69c30a653b573fed52310cc84faa056f230b0d7ea62a0b457534e0.json b/.sqlx/query-eeaf29b5efeb08c4729dec89f1e76c817a53bbf99998c5b1e428227d1b223b0f.json similarity index 75% rename from .sqlx/query-f6aede22ec69c30a653b573fed52310cc84faa056f230b0d7ea62a0b457534e0.json rename to .sqlx/query-eeaf29b5efeb08c4729dec89f1e76c817a53bbf99998c5b1e428227d1b223b0f.json index 5f96327..484db6b 100644 --- a/.sqlx/query-f6aede22ec69c30a653b573fed52310cc84faa056f230b0d7ea62a0b457534e0.json +++ b/.sqlx/query-eeaf29b5efeb08c4729dec89f1e76c817a53bbf99998c5b1e428227d1b223b0f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, did, email, password_hash, two_factor_enabled,\n preferred_comms_channel as \"preferred_comms_channel: CommsChannel\",\n deactivated_at, takedown_ref,\n email_verified, discord_verified, telegram_verified, signal_verified\n FROM users\n WHERE handle = $1 OR email = $1\n ", + "query": "\n SELECT id, did, email, password_hash, password_required, two_factor_enabled,\n preferred_comms_channel as \"preferred_comms_channel: CommsChannel\",\n deactivated_at, takedown_ref,\n email_verified, discord_verified, telegram_verified, signal_verified\n FROM users\n WHERE handle = $1 OR email = $1\n ", "describe": { "columns": [ { @@ -25,11 +25,16 @@ }, { "ordinal": 4, - "name": "two_factor_enabled", + "name": "password_required", "type_info": "Bool" }, { "ordinal": 5, + "name": "two_factor_enabled", + "type_info": "Bool" + }, + { + "ordinal": 6, "name": "preferred_comms_channel: CommsChannel", "type_info": { "Custom": { @@ -46,32 +51,32 @@ } }, { - "ordinal": 6, + "ordinal": 7, "name": "deactivated_at", "type_info": "Timestamptz" }, { - "ordinal": 7, + "ordinal": 8, "name": "takedown_ref", "type_info": "Text" }, { - "ordinal": 8, + "ordinal": 9, "name": "email_verified", "type_info": "Bool" }, { - "ordinal": 9, + "ordinal": 10, "name": "discord_verified", "type_info": "Bool" }, { - "ordinal": 10, + "ordinal": 11, "name": "telegram_verified", "type_info": "Bool" }, { - "ordinal": 11, + "ordinal": 12, "name": "signal_verified", "type_info": "Bool" } @@ -85,6 +90,7 @@ false, false, true, + true, false, false, false, @@ -96,5 +102,5 @@ false ] }, - "hash": "f6aede22ec69c30a653b573fed52310cc84faa056f230b0d7ea62a0b457534e0" + "hash": "eeaf29b5efeb08c4729dec89f1e76c817a53bbf99998c5b1e428227d1b223b0f" } diff --git a/.sqlx/query-f6ece5d279114e72f575229979e1123f1c4e0cfa721449a3f4a495e6c3ce0289.json b/.sqlx/query-f6ece5d279114e72f575229979e1123f1c4e0cfa721449a3f4a495e6c3ce0289.json new file mode 100644 index 0000000..7dcbcbe --- /dev/null +++ b/.sqlx/query-f6ece5d279114e72f575229979e1123f1c4e0cfa721449a3f4a495e6c3ce0289.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, handle, recovery_token, recovery_token_expires_at, password_required\n FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "handle", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "recovery_token", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "recovery_token_expires_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "password_required", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + true, + true, + false + ] + }, + "hash": "f6ece5d279114e72f575229979e1123f1c4e0cfa721449a3f4a495e6c3ce0289" +} diff --git a/.sqlx/query-fcf8ca1f6261521bcbf4dbfdbfaf69e242cd9c16687fa9a72a618d57c8f0d9ba.json b/.sqlx/query-fcf8ca1f6261521bcbf4dbfdbfaf69e242cd9c16687fa9a72a618d57c8f0d9ba.json new file mode 100644 index 0000000..617adf8 --- /dev/null +++ b/.sqlx/query-fcf8ca1f6261521bcbf4dbfdbfaf69e242cd9c16687fa9a72a618d57c8f0d9ba.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT password_hash IS NOT NULL as has_pw FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "has_pw", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "fcf8ca1f6261521bcbf4dbfdbfaf69e242cd9c16687fa9a72a618d57c8f0d9ba" +} diff --git a/.sqlx/query-fde01bb40898f8a5d45a6e8f89c635c06b4179b5858a7b388404c4b03fc92ab4.json b/.sqlx/query-fde01bb40898f8a5d45a6e8f89c635c06b4179b5858a7b388404c4b03fc92ab4.json index 9e183a2..19e35a1 100644 --- a/.sqlx/query-fde01bb40898f8a5d45a6e8f89c635c06b4179b5858a7b388404c4b03fc92ab4.json +++ b/.sqlx/query-fde01bb40898f8a5d45a6e8f89c635c06b4179b5858a7b388404c4b03fc92ab4.json @@ -41,7 +41,8 @@ "admin_email", "plc_operation", "two_factor_code", - "channel_verification" + "channel_verification", + "passkey_recovery" ] } } diff --git a/.sqlx/query-fdff88b03b8fe4679e29b06b3cfa386c68f8539725e8558643889a4ef92067b4.json b/.sqlx/query-fdff88b03b8fe4679e29b06b3cfa386c68f8539725e8558643889a4ef92067b4.json new file mode 100644 index 0000000..96c6668 --- /dev/null +++ b/.sqlx/query-fdff88b03b8fe4679e29b06b3cfa386c68f8539725e8558643889a4ef92067b4.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, preferred_comms_channel as \"preferred_comms_channel: CommsChannel\" FROM users WHERE did = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "preferred_comms_channel: CommsChannel", + "type_info": { + "Custom": { + "name": "comms_channel", + "kind": { + "Enum": [ + "email", + "discord", + "telegram", + "signal" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "fdff88b03b8fe4679e29b06b3cfa386c68f8539725e8558643889a4ef92067b4" +} diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index bd291e9..0586495 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -3,8 +3,11 @@ import { initAuth, getAuthState } from './lib/auth.svelte' import Login from './routes/Login.svelte' import Register from './routes/Register.svelte' + import RegisterPasskey from './routes/RegisterPasskey.svelte' import Verify from './routes/Verify.svelte' import ResetPassword from './routes/ResetPassword.svelte' + import RecoverPasskey from './routes/RecoverPasskey.svelte' + import RequestPasskeyRecovery from './routes/RequestPasskeyRecovery.svelte' import Dashboard from './routes/Dashboard.svelte' import AppPasswords from './routes/AppPasswords.svelte' import InviteCodes from './routes/InviteCodes.svelte' @@ -18,8 +21,10 @@ import OAuthAccounts from './routes/OAuthAccounts.svelte' import OAuth2FA from './routes/OAuth2FA.svelte' import OAuthTotp from './routes/OAuthTotp.svelte' + import OAuthPasskey from './routes/OAuthPasskey.svelte' import OAuthError from './routes/OAuthError.svelte' import Security from './routes/Security.svelte' + import TrustedDevices from './routes/TrustedDevices.svelte' const auth = getAuthState() @@ -33,10 +38,16 @@ return Login case '/register': return Register + case '/register-passkey': + return RegisterPasskey case '/verify': return Verify case '/reset-password': return ResetPassword + case '/recover-passkey': + return RecoverPasskey + case '/request-passkey-recovery': + return RequestPasskeyRecovery case '/dashboard': return Dashboard case '/app-passwords': @@ -63,10 +74,14 @@ return OAuth2FA case '/oauth/totp': return OAuthTotp + case '/oauth/passkey': + return OAuthPasskey case '/oauth/error': return OAuthError case '/security': return Security + case '/trusted-devices': + return TrustedDevices default: return auth.session ? Dashboard : Login } diff --git a/frontend/src/components/ReauthModal.svelte b/frontend/src/components/ReauthModal.svelte new file mode 100644 index 0000000..264a372 --- /dev/null +++ b/frontend/src/components/ReauthModal.svelte @@ -0,0 +1,430 @@ + + +{#if show} +
+ This action requires you to verify your identity. +
+ + {#if error} + + {/if} + + {#if availableMethods.length > 1} +- Forgot password? + Forgot password? · Lost passkey?
Don't have an account? Create one diff --git a/frontend/src/routes/OAuthConsent.svelte b/frontend/src/routes/OAuthConsent.svelte index 3cf35db..efb00a2 100644 --- a/frontend/src/routes/OAuthConsent.svelte +++ b/frontend/src/routes/OAuthConsent.svelte @@ -115,8 +115,8 @@ try { const response = await fetch('/oauth/authorize/deny', { method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: `request_uri=${encodeURIComponent(consentData.request_uri)}` + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ request_uri: consentData.request_uri }) }) if (response.redirected) { diff --git a/frontend/src/routes/OAuthLogin.svelte b/frontend/src/routes/OAuthLogin.svelte index 001815d..e6ccaf3 100644 --- a/frontend/src/routes/OAuthLogin.svelte +++ b/frontend/src/routes/OAuthLogin.svelte @@ -399,9 +399,28 @@ + +
+ Forgot password? · Lost passkey? +
diff --git a/frontend/src/routes/OAuthTotp.svelte b/frontend/src/routes/OAuthTotp.svelte index dc5935a..14d8730 100644 --- a/frontend/src/routes/OAuthTotp.svelte +++ b/frontend/src/routes/OAuthTotp.svelte @@ -2,6 +2,7 @@ import { navigate } from '../lib/router.svelte' let code = $state('') + let trustDevice = $state(false) let submitting = $state(false) let error = $state+ If your account exists and is a passkey-only account, you'll receive a recovery link + at your preferred notification channel. +
++ The link will expire in 1 hour. Check your email, Discord, Telegram, or Signal + depending on your account settings. +
++ Lost access to your passkey? Enter your handle or email and we'll send you a recovery link. +
+ + {#if error} ++ Back to Sign In +
+Enter the code from your email and choose a new password.
+Enter the code you received and choose a new password.