diff --git a/.sqlx/query-17bd3bd354a6ee0a86a1c868207eb4ea454844828c8aca63b1252fefa8f5afad.json b/.sqlx/query-17bd3bd354a6ee0a86a1c868207eb4ea454844828c8aca63b1252fefa8f5afad.json
new file mode 100644
index 0000000..b3d3bae
--- /dev/null
+++ b/.sqlx/query-17bd3bd354a6ee0a86a1c868207eb4ea454844828c8aca63b1252fefa8f5afad.json
@@ -0,0 +1,14 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE comms_queue\n SET status = 'sent', processed_at = NOW(), updated_at = NOW()\n WHERE id = $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Uuid"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "17bd3bd354a6ee0a86a1c868207eb4ea454844828c8aca63b1252fefa8f5afad"
+}
diff --git a/.sqlx/query-5d49bbf0307a0c642b0174d641de748fa648c97f8109255120e969c957ff95bf.json b/.sqlx/query-17dfafc85b3434ed78041f48809580a02c92e579869f647cb08f65ac777854f5.json
similarity index 72%
rename from .sqlx/query-5d49bbf0307a0c642b0174d641de748fa648c97f8109255120e969c957ff95bf.json
rename to .sqlx/query-17dfafc85b3434ed78041f48809580a02c92e579869f647cb08f65ac777854f5.json
index ed46759..4444b64 100644
--- a/.sqlx/query-5d49bbf0307a0c642b0174d641de748fa648c97f8109255120e969c957ff95bf.json
+++ b/.sqlx/query-17dfafc85b3434ed78041f48809580a02c92e579869f647cb08f65ac777854f5.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n INSERT INTO notification_queue\n (user_id, channel, notification_type, recipient, subject, body, metadata)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id\n ",
+ "query": "\n INSERT INTO comms_queue\n (user_id, channel, comms_type, recipient, subject, body, metadata)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id\n ",
"describe": {
"columns": [
{
@@ -14,7 +14,7 @@
"Uuid",
{
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -27,7 +27,7 @@
},
{
"Custom": {
- "name": "notification_type",
+ "name": "comms_type",
"kind": {
"Enum": [
"welcome",
@@ -53,5 +53,5 @@
false
]
},
- "hash": "5d49bbf0307a0c642b0174d641de748fa648c97f8109255120e969c957ff95bf"
+ "hash": "17dfafc85b3434ed78041f48809580a02c92e579869f647cb08f65ac777854f5"
}
diff --git a/.sqlx/query-cb6f48aaba124c79308d20e66c23adb44d1196296b7f93fad19b2d17548ed3de.json b/.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json
similarity index 68%
rename from .sqlx/query-cb6f48aaba124c79308d20e66c23adb44d1196296b7f93fad19b2d17548ed3de.json
rename to .sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json
index dde296d..7004e69 100644
--- a/.sqlx/query-cb6f48aaba124c79308d20e66c23adb44d1196296b7f93fad19b2d17548ed3de.json
+++ b/.sqlx/query-20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n UPDATE notification_queue\n SET status = 'processing', updated_at = NOW()\n WHERE id IN (\n SELECT id FROM notification_queue\n WHERE status = 'pending'\n AND scheduled_for <= $1\n AND attempts < max_attempts\n ORDER BY scheduled_for ASC\n LIMIT $2\n FOR UPDATE SKIP LOCKED\n )\n RETURNING\n id, user_id,\n channel as \"channel: NotificationChannel\",\n notification_type as \"notification_type: super::types::NotificationType\",\n status as \"status: NotificationStatus\",\n recipient, subject, body, metadata,\n attempts, max_attempts, last_error,\n created_at, updated_at, scheduled_for, processed_at\n ",
+ "query": "\n UPDATE comms_queue\n SET status = 'processing', updated_at = NOW()\n WHERE id IN (\n SELECT id FROM comms_queue\n WHERE status = 'pending'\n AND scheduled_for <= $1\n AND attempts < max_attempts\n ORDER BY scheduled_for ASC\n LIMIT $2\n FOR UPDATE SKIP LOCKED\n )\n RETURNING\n id, user_id,\n channel as \"channel: CommsChannel\",\n comms_type as \"comms_type: super::types::CommsType\",\n status as \"status: CommsStatus\",\n recipient, subject, body, metadata,\n attempts, max_attempts, last_error,\n created_at, updated_at, scheduled_for, processed_at\n ",
"describe": {
"columns": [
{
@@ -15,10 +15,10 @@
},
{
"ordinal": 2,
- "name": "channel: NotificationChannel",
+ "name": "channel: CommsChannel",
"type_info": {
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -32,10 +32,10 @@
},
{
"ordinal": 3,
- "name": "notification_type: super::types::NotificationType",
+ "name": "comms_type: super::types::CommsType",
"type_info": {
"Custom": {
- "name": "notification_type",
+ "name": "comms_type",
"kind": {
"Enum": [
"welcome",
@@ -54,10 +54,10 @@
},
{
"ordinal": 4,
- "name": "status: NotificationStatus",
+ "name": "status: CommsStatus",
"type_info": {
"Custom": {
- "name": "notification_status",
+ "name": "comms_status",
"kind": {
"Enum": [
"pending",
@@ -150,5 +150,5 @@
true
]
},
- "hash": "cb6f48aaba124c79308d20e66c23adb44d1196296b7f93fad19b2d17548ed3de"
+ "hash": "20dd204aa552572ec9dc5b9950efdfa8a2e37aae3f171a2be73bee3057f86e08"
}
diff --git a/.sqlx/query-2c6cb8f15fe71cb5f38ffd7f5085b60bc852c4f1042c95a76fce773efd369511.json b/.sqlx/query-2c6cb8f15fe71cb5f38ffd7f5085b60bc852c4f1042c95a76fce773efd369511.json
deleted file mode 100644
index 872ce2a..0000000
--- a/.sqlx/query-2c6cb8f15fe71cb5f38ffd7f5085b60bc852c4f1042c95a76fce773efd369511.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n UPDATE notification_queue\n SET\n status = CASE\n WHEN attempts + 1 >= max_attempts THEN 'failed'::notification_status\n ELSE 'pending'::notification_status\n END,\n attempts = attempts + 1,\n last_error = $2,\n updated_at = NOW(),\n scheduled_for = NOW() + (INTERVAL '1 minute' * (attempts + 1))\n WHERE id = $1\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Uuid",
- "Text"
- ]
- },
- "nullable": []
- },
- "hash": "2c6cb8f15fe71cb5f38ffd7f5085b60bc852c4f1042c95a76fce773efd369511"
-}
diff --git a/.sqlx/query-344c851d3f1b026e8632aa2f04052dcbc957b7077c856da6a1a256ec2fe85ad3.json b/.sqlx/query-344c851d3f1b026e8632aa2f04052dcbc957b7077c856da6a1a256ec2fe85ad3.json
deleted file mode 100644
index 7abae00..0000000
--- a/.sqlx/query-344c851d3f1b026e8632aa2f04052dcbc957b7077c856da6a1a256ec2fe85ad3.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n UPDATE notification_queue\n SET status = 'sent', processed_at = NOW(), updated_at = NOW()\n WHERE id = $1\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Uuid"
- ]
- },
- "nullable": []
- },
- "hash": "344c851d3f1b026e8632aa2f04052dcbc957b7077c856da6a1a256ec2fe85ad3"
-}
diff --git a/.sqlx/query-303777d97e6ed344f8c699eae37b7b0c241c734a5b7726019c2a59ae277caee6.json b/.sqlx/query-3f9b3b06f54df7c1d20ea9ff94b914ad3bf77d47dd393a0aae1c030b8ce98bcc.json
similarity index 71%
rename from .sqlx/query-303777d97e6ed344f8c699eae37b7b0c241c734a5b7726019c2a59ae277caee6.json
rename to .sqlx/query-3f9b3b06f54df7c1d20ea9ff94b914ad3bf77d47dd393a0aae1c030b8ce98bcc.json
index 5d80a15..d87a8a1 100644
--- a/.sqlx/query-303777d97e6ed344f8c699eae37b7b0c241c734a5b7726019c2a59ae277caee6.json
+++ b/.sqlx/query-3f9b3b06f54df7c1d20ea9ff94b914ad3bf77d47dd393a0aae1c030b8ce98bcc.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n INSERT INTO notification_queue\n (user_id, channel, notification_type, recipient, subject, body, metadata)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id\n ",
+ "query": "\n INSERT INTO comms_queue\n (user_id, channel, comms_type, recipient, subject, body, metadata)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id\n ",
"describe": {
"columns": [
{
@@ -14,7 +14,7 @@
"Uuid",
{
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -27,7 +27,7 @@
},
{
"Custom": {
- "name": "notification_type",
+ "name": "comms_type",
"kind": {
"Enum": [
"welcome",
@@ -53,5 +53,5 @@
false
]
},
- "hash": "303777d97e6ed344f8c699eae37b7b0c241c734a5b7726019c2a59ae277caee6"
+ "hash": "3f9b3b06f54df7c1d20ea9ff94b914ad3bf77d47dd393a0aae1c030b8ce98bcc"
}
diff --git a/.sqlx/query-ae85520d67815e95802c0e28db120c3c10badee74f78722d3cea58d183734bf6.json b/.sqlx/query-4a77184e491ed1f011966fd7fa1332bfeaf782a7787784008f15254c02ef57d5.json
similarity index 76%
rename from .sqlx/query-ae85520d67815e95802c0e28db120c3c10badee74f78722d3cea58d183734bf6.json
rename to .sqlx/query-4a77184e491ed1f011966fd7fa1332bfeaf782a7787784008f15254c02ef57d5.json
index c494da4..943b4d9 100644
--- a/.sqlx/query-ae85520d67815e95802c0e28db120c3c10badee74f78722d3cea58d183734bf6.json
+++ b/.sqlx/query-4a77184e491ed1f011966fd7fa1332bfeaf782a7787784008f15254c02ef57d5.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT\n id, handle, email,\n preferred_notification_channel as \"channel: crate::notifications::NotificationChannel\",\n discord_id, telegram_username, signal_number,\n email_confirmed, discord_verified, telegram_verified, signal_verified\n FROM users\n WHERE did = $1",
+ "query": "SELECT\n id, handle, email,\n preferred_comms_channel as \"channel: crate::comms::CommsChannel\",\n discord_id, telegram_username, signal_number,\n email_verified, discord_verified, telegram_verified, signal_verified\n FROM users\n WHERE did = $1",
"describe": {
"columns": [
{
@@ -20,10 +20,10 @@
},
{
"ordinal": 3,
- "name": "channel: crate::notifications::NotificationChannel",
+ "name": "channel: crate::comms::CommsChannel",
"type_info": {
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -52,7 +52,7 @@
},
{
"ordinal": 7,
- "name": "email_confirmed",
+ "name": "email_verified",
"type_info": "Bool"
},
{
@@ -90,5 +90,5 @@
false
]
},
- "hash": "ae85520d67815e95802c0e28db120c3c10badee74f78722d3cea58d183734bf6"
+ "hash": "4a77184e491ed1f011966fd7fa1332bfeaf782a7787784008f15254c02ef57d5"
}
diff --git a/.sqlx/query-90f5c38a28537b2deddd0f897b8902a97547983851bd95a9ae496943064b1849.json b/.sqlx/query-57229564a518b14dca6fecef677d4b58b5ab6892846e65a4f1549ae5f147c13e.json
similarity index 73%
rename from .sqlx/query-90f5c38a28537b2deddd0f897b8902a97547983851bd95a9ae496943064b1849.json
rename to .sqlx/query-57229564a518b14dca6fecef677d4b58b5ab6892846e65a4f1549ae5f147c13e.json
index 11ac47d..9244ba0 100644
--- a/.sqlx/query-90f5c38a28537b2deddd0f897b8902a97547983851bd95a9ae496943064b1849.json
+++ b/.sqlx/query-57229564a518b14dca6fecef677d4b58b5ab6892846e65a4f1549ae5f147c13e.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "DELETE FROM channel_verifications WHERE user_id = $1 AND channel = $2::notification_channel",
+ "query": "DELETE FROM channel_verifications WHERE user_id = $1 AND channel = $2::comms_channel",
"describe": {
"columns": [],
"parameters": {
@@ -8,7 +8,7 @@
"Uuid",
{
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -23,5 +23,5 @@
},
"nullable": []
},
- "hash": "90f5c38a28537b2deddd0f897b8902a97547983851bd95a9ae496943064b1849"
+ "hash": "57229564a518b14dca6fecef677d4b58b5ab6892846e65a4f1549ae5f147c13e"
}
diff --git a/.sqlx/query-62f66fad54498d5c598af54de795e395f71596a6d6a88d2be64ce86256a9860f.json b/.sqlx/query-62f66fad54498d5c598af54de795e395f71596a6d6a88d2be64ce86256a9860f.json
deleted file mode 100644
index 4b9b1db..0000000
--- a/.sqlx/query-62f66fad54498d5c598af54de795e395f71596a6d6a88d2be64ce86256a9860f.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n SELECT id, two_factor_enabled,\n preferred_notification_channel as \"preferred_notification_channel: NotificationChannel\"\n FROM users\n WHERE did = $1\n ",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "id",
- "type_info": "Uuid"
- },
- {
- "ordinal": 1,
- "name": "two_factor_enabled",
- "type_info": "Bool"
- },
- {
- "ordinal": 2,
- "name": "preferred_notification_channel: NotificationChannel",
- "type_info": {
- "Custom": {
- "name": "notification_channel",
- "kind": {
- "Enum": [
- "email",
- "discord",
- "telegram",
- "signal"
- ]
- }
- }
- }
- }
- ],
- "parameters": {
- "Left": [
- "Text"
- ]
- },
- "nullable": [
- false,
- false,
- false
- ]
- },
- "hash": "62f66fad54498d5c598af54de795e395f71596a6d6a88d2be64ce86256a9860f"
-}
diff --git a/.sqlx/query-64510156f2b79cdc41f08867952abbea919b9a90167958f018ceb9972b9e8230.json b/.sqlx/query-64510156f2b79cdc41f08867952abbea919b9a90167958f018ceb9972b9e8230.json
new file mode 100644
index 0000000..08fa520
--- /dev/null
+++ b/.sqlx/query-64510156f2b79cdc41f08867952abbea919b9a90167958f018ceb9972b9e8230.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE comms_queue\n SET\n status = CASE\n WHEN attempts + 1 >= max_attempts THEN 'failed'::comms_status\n ELSE 'pending'::comms_status\n END,\n attempts = attempts + 1,\n last_error = $2,\n updated_at = NOW(),\n scheduled_for = NOW() + (INTERVAL '1 minute' * (attempts + 1))\n WHERE id = $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Uuid",
+ "Text"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "64510156f2b79cdc41f08867952abbea919b9a90167958f018ceb9972b9e8230"
+}
diff --git a/.sqlx/query-bfb9ee0187a0062cb83c9295cf266f56fed0edd0f9f154c1786f2b0cdbe39508.json b/.sqlx/query-8c69c5f98e3ee59b50346094ff39eed73bb602f0b5ab48c11e53c82839a66721.json
similarity index 70%
rename from .sqlx/query-bfb9ee0187a0062cb83c9295cf266f56fed0edd0f9f154c1786f2b0cdbe39508.json
rename to .sqlx/query-8c69c5f98e3ee59b50346094ff39eed73bb602f0b5ab48c11e53c82839a66721.json
index b0a5d1b..4dfc823 100644
--- a/.sqlx/query-bfb9ee0187a0062cb83c9295cf266f56fed0edd0f9f154c1786f2b0cdbe39508.json
+++ b/.sqlx/query-8c69c5f98e3ee59b50346094ff39eed73bb602f0b5ab48c11e53c82839a66721.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n email,\n handle,\n preferred_notification_channel as \"channel: NotificationChannel\"\n FROM users\n WHERE id = $1\n ",
+ "query": "\n SELECT\n email,\n handle,\n preferred_comms_channel as \"channel: CommsChannel\"\n FROM users\n WHERE id = $1\n ",
"describe": {
"columns": [
{
@@ -15,10 +15,10 @@
},
{
"ordinal": 2,
- "name": "channel: NotificationChannel",
+ "name": "channel: CommsChannel",
"type_info": {
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -42,5 +42,5 @@
false
]
},
- "hash": "bfb9ee0187a0062cb83c9295cf266f56fed0edd0f9f154c1786f2b0cdbe39508"
+ "hash": "8c69c5f98e3ee59b50346094ff39eed73bb602f0b5ab48c11e53c82839a66721"
}
diff --git a/.sqlx/query-0cbeeffaf2cf782de4e9d886e26b9884e874735e76b50c42933a94d9fa70425e.json b/.sqlx/query-94966f20b7b0adb02e8c83a693a4dcc7f54b72983ba8ebd66fd805851db5c06c.json
similarity index 62%
rename from .sqlx/query-0cbeeffaf2cf782de4e9d886e26b9884e874735e76b50c42933a94d9fa70425e.json
rename to .sqlx/query-94966f20b7b0adb02e8c83a693a4dcc7f54b72983ba8ebd66fd805851db5c06c.json
index 74512ad..259fb57 100644
--- a/.sqlx/query-0cbeeffaf2cf782de4e9d886e26b9884e874735e76b50c42933a94d9fa70425e.json
+++ b/.sqlx/query-94966f20b7b0adb02e8c83a693a4dcc7f54b72983ba8ebd66fd805851db5c06c.json
@@ -1,14 +1,14 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT preferred_notification_channel as \"channel: NotificationChannel\" FROM users WHERE did = $1",
+ "query": "SELECT preferred_comms_channel as \"channel: CommsChannel\" FROM users WHERE did = $1",
"describe": {
"columns": [
{
"ordinal": 0,
- "name": "channel: NotificationChannel",
+ "name": "channel: CommsChannel",
"type_info": {
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -30,5 +30,5 @@
false
]
},
- "hash": "0cbeeffaf2cf782de4e9d886e26b9884e874735e76b50c42933a94d9fa70425e"
+ "hash": "94966f20b7b0adb02e8c83a693a4dcc7f54b72983ba8ebd66fd805851db5c06c"
}
diff --git a/.sqlx/query-9ebca49cb60b1891d3c1ef0087e189f2e35b982fce0c3313748c23c113a6e546.json b/.sqlx/query-c4db3853b2f3b6363ab0e2c10a1820dd37741e9c506d85fc2608a3a6e376c5e6.json
similarity index 64%
rename from .sqlx/query-9ebca49cb60b1891d3c1ef0087e189f2e35b982fce0c3313748c23c113a6e546.json
rename to .sqlx/query-c4db3853b2f3b6363ab0e2c10a1820dd37741e9c506d85fc2608a3a6e376c5e6.json
index 84c9334..bba38eb 100644
--- a/.sqlx/query-9ebca49cb60b1891d3c1ef0087e189f2e35b982fce0c3313748c23c113a6e546.json
+++ b/.sqlx/query-c4db3853b2f3b6363ab0e2c10a1820dd37741e9c506d85fc2608a3a6e376c5e6.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at)\n VALUES ($1, $2::notification_channel, $3, $4, $5)\n ON CONFLICT (user_id, channel) DO UPDATE\n SET code = $3, pending_identifier = $4, expires_at = $5, created_at = NOW()\n ",
+ "query": "\n INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at)\n VALUES ($1, $2::comms_channel, $3, $4, $5)\n ON CONFLICT (user_id, channel) DO UPDATE\n SET code = $3, pending_identifier = $4, expires_at = $5, created_at = NOW()\n ",
"describe": {
"columns": [],
"parameters": {
@@ -8,7 +8,7 @@
"Uuid",
{
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -26,5 +26,5 @@
},
"nullable": []
},
- "hash": "9ebca49cb60b1891d3c1ef0087e189f2e35b982fce0c3313748c23c113a6e546"
+ "hash": "c4db3853b2f3b6363ab0e2c10a1820dd37741e9c506d85fc2608a3a6e376c5e6"
}
diff --git a/.sqlx/query-d41e1b7d5e22c06896ae28c6790d5c7c8e6a7c9489133bb9357d012d7a75813b.json b/.sqlx/query-d41e1b7d5e22c06896ae28c6790d5c7c8e6a7c9489133bb9357d012d7a75813b.json
new file mode 100644
index 0000000..c62fab5
--- /dev/null
+++ b/.sqlx/query-d41e1b7d5e22c06896ae28c6790d5c7c8e6a7c9489133bb9357d012d7a75813b.json
@@ -0,0 +1,34 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT u.id, uk.key_bytes, uk.encryption_version\n FROM users u\n JOIN user_keys uk ON u.id = uk.user_id\n WHERE u.did = $1",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Uuid"
+ },
+ {
+ "ordinal": 1,
+ "name": "key_bytes",
+ "type_info": "Bytea"
+ },
+ {
+ "ordinal": 2,
+ "name": "encryption_version",
+ "type_info": "Int4"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Text"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ true
+ ]
+ },
+ "hash": "d41e1b7d5e22c06896ae28c6790d5c7c8e6a7c9489133bb9357d012d7a75813b"
+}
diff --git a/.sqlx/query-1f1d099cc5f5800a939c03b60b24e889c615bb4dab0895863fd59c913f7895fd.json b/.sqlx/query-d61c982dac3a508393b31a30bad50c0088ce6e117fe63c5a1062a97000dedf89.json
similarity index 77%
rename from .sqlx/query-1f1d099cc5f5800a939c03b60b24e889c615bb4dab0895863fd59c913f7895fd.json
rename to .sqlx/query-d61c982dac3a508393b31a30bad50c0088ce6e117fe63c5a1062a97000dedf89.json
index dbd6434..23b2e51 100644
--- a/.sqlx/query-1f1d099cc5f5800a939c03b60b24e889c615bb4dab0895863fd59c913f7895fd.json
+++ b/.sqlx/query-d61c982dac3a508393b31a30bad50c0088ce6e117fe63c5a1062a97000dedf89.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT\n u.id, u.did, u.handle, u.password_hash,\n u.email_confirmed, u.discord_verified, u.telegram_verified, u.signal_verified,\n k.key_bytes, k.encryption_version\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1",
+ "query": "SELECT\n u.id, u.did, u.handle, u.password_hash,\n u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,\n k.key_bytes, k.encryption_version\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.handle = $1 OR u.email = $1",
"describe": {
"columns": [
{
@@ -25,7 +25,7 @@
},
{
"ordinal": 4,
- "name": "email_confirmed",
+ "name": "email_verified",
"type_info": "Bool"
},
{
@@ -72,5 +72,5 @@
true
]
},
- "hash": "1f1d099cc5f5800a939c03b60b24e889c615bb4dab0895863fd59c913f7895fd"
+ "hash": "d61c982dac3a508393b31a30bad50c0088ce6e117fe63c5a1062a97000dedf89"
}
diff --git a/.sqlx/query-daa235d54827ca9b2803da732d3d35c6012b7cc6aac81c5e46be9d24cfd42c24.json b/.sqlx/query-daa235d54827ca9b2803da732d3d35c6012b7cc6aac81c5e46be9d24cfd42c24.json
new file mode 100644
index 0000000..1ce095f
--- /dev/null
+++ b/.sqlx/query-daa235d54827ca9b2803da732d3d35c6012b7cc6aac81c5e46be9d24cfd42c24.json
@@ -0,0 +1,70 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT id, two_factor_enabled,\n preferred_comms_channel as \"preferred_comms_channel: CommsChannel\",\n email_verified, discord_verified, telegram_verified, signal_verified\n FROM users\n WHERE did = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Uuid"
+ },
+ {
+ "ordinal": 1,
+ "name": "two_factor_enabled",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 2,
+ "name": "preferred_comms_channel: CommsChannel",
+ "type_info": {
+ "Custom": {
+ "name": "comms_channel",
+ "kind": {
+ "Enum": [
+ "email",
+ "discord",
+ "telegram",
+ "signal"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "ordinal": 3,
+ "name": "email_verified",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 4,
+ "name": "discord_verified",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 5,
+ "name": "telegram_verified",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 6,
+ "name": "signal_verified",
+ "type_info": "Bool"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Text"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false
+ ]
+ },
+ "hash": "daa235d54827ca9b2803da732d3d35c6012b7cc6aac81c5e46be9d24cfd42c24"
+}
diff --git a/.sqlx/query-088e0b03c2f706402d474e4431562a40a64a5ce575bd8884b2c5d51f04871de1.json b/.sqlx/query-de72338f80b4f7b5bc7c9fc44100b6eb9e75f442b5b37a5a1cd761cd3b6950d9.json
similarity index 69%
rename from .sqlx/query-088e0b03c2f706402d474e4431562a40a64a5ce575bd8884b2c5d51f04871de1.json
rename to .sqlx/query-de72338f80b4f7b5bc7c9fc44100b6eb9e75f442b5b37a5a1cd761cd3b6950d9.json
index 5690886..64edd35 100644
--- a/.sqlx/query-088e0b03c2f706402d474e4431562a40a64a5ce575bd8884b2c5d51f04871de1.json
+++ b/.sqlx/query-de72338f80b4f7b5bc7c9fc44100b6eb9e75f442b5b37a5a1cd761cd3b6950d9.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT\n handle, email, email_confirmed, is_admin,\n preferred_notification_channel as \"preferred_channel: crate::notifications::NotificationChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
+ "query": "SELECT\n handle, email, email_verified, is_admin,\n preferred_comms_channel as \"preferred_channel: crate::comms::CommsChannel\",\n discord_verified, telegram_verified, signal_verified\n FROM users WHERE did = $1",
"describe": {
"columns": [
{
@@ -15,7 +15,7 @@
},
{
"ordinal": 2,
- "name": "email_confirmed",
+ "name": "email_verified",
"type_info": "Bool"
},
{
@@ -25,10 +25,10 @@
},
{
"ordinal": 4,
- "name": "preferred_channel: crate::notifications::NotificationChannel",
+ "name": "preferred_channel: crate::comms::CommsChannel",
"type_info": {
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -72,5 +72,5 @@
false
]
},
- "hash": "088e0b03c2f706402d474e4431562a40a64a5ce575bd8884b2c5d51f04871de1"
+ "hash": "de72338f80b4f7b5bc7c9fc44100b6eb9e75f442b5b37a5a1cd761cd3b6950d9"
}
diff --git a/.sqlx/query-0bb332a1a1b648aaeca02415b8d2fc39c045bac9b3897b1c886b6ffc68538bac.json b/.sqlx/query-e774d655b838c219c8291a5bc8e6fb90b793c78402c648dd380538b6e2b47134.json
similarity index 53%
rename from .sqlx/query-0bb332a1a1b648aaeca02415b8d2fc39c045bac9b3897b1c886b6ffc68538bac.json
rename to .sqlx/query-e774d655b838c219c8291a5bc8e6fb90b793c78402c648dd380538b6e2b47134.json
index f98e023..d5e3776 100644
--- a/.sqlx/query-0bb332a1a1b648aaeca02415b8d2fc39c045bac9b3897b1c886b6ffc68538bac.json
+++ b/.sqlx/query-e774d655b838c219c8291a5bc8e6fb90b793c78402c648dd380538b6e2b47134.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n INSERT INTO notification_queue (user_id, channel, notification_type, recipient, subject, body, metadata)\n VALUES ($1, $2::notification_channel, 'channel_verification', $3, 'Verify your channel', $4, $5)\n ",
+ "query": "\n INSERT INTO comms_queue (user_id, channel, comms_type, recipient, subject, body, metadata)\n VALUES ($1, $2::comms_channel, 'channel_verification', $3, 'Verify your channel', $4, $5)\n ",
"describe": {
"columns": [],
"parameters": {
@@ -8,7 +8,7 @@
"Uuid",
{
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -26,5 +26,5 @@
},
"nullable": []
},
- "hash": "0bb332a1a1b648aaeca02415b8d2fc39c045bac9b3897b1c886b6ffc68538bac"
+ "hash": "e774d655b838c219c8291a5bc8e6fb90b793c78402c648dd380538b6e2b47134"
}
diff --git a/.sqlx/query-dfcfb9ccc41c389bf06548f815080e7601c636ddce2b5a04a2d33a19461a2fe3.json b/.sqlx/query-efc26a1202b1bbf72da1c06b59b47e560dfb5912db8e40ee92cf91846f306e1a.json
similarity index 72%
rename from .sqlx/query-dfcfb9ccc41c389bf06548f815080e7601c636ddce2b5a04a2d33a19461a2fe3.json
rename to .sqlx/query-efc26a1202b1bbf72da1c06b59b47e560dfb5912db8e40ee92cf91846f306e1a.json
index 05577b6..b98bfef 100644
--- a/.sqlx/query-dfcfb9ccc41c389bf06548f815080e7601c636ddce2b5a04a2d33a19461a2fe3.json
+++ b/.sqlx/query-efc26a1202b1bbf72da1c06b59b47e560dfb5912db8e40ee92cf91846f306e1a.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "SELECT\n u.id, u.did, u.handle, u.email,\n u.preferred_notification_channel as \"channel: crate::notifications::NotificationChannel\",\n k.key_bytes, k.encryption_version\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1",
+ "query": "SELECT\n u.id, u.did, u.handle, u.email,\n u.preferred_comms_channel as \"channel: crate::comms::CommsChannel\",\n k.key_bytes, k.encryption_version\n FROM users u\n JOIN user_keys k ON u.id = k.user_id\n WHERE u.did = $1",
"describe": {
"columns": [
{
@@ -25,10 +25,10 @@
},
{
"ordinal": 4,
- "name": "channel: crate::notifications::NotificationChannel",
+ "name": "channel: crate::comms::CommsChannel",
"type_info": {
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -66,5 +66,5 @@
true
]
},
- "hash": "dfcfb9ccc41c389bf06548f815080e7601c636ddce2b5a04a2d33a19461a2fe3"
+ "hash": "efc26a1202b1bbf72da1c06b59b47e560dfb5912db8e40ee92cf91846f306e1a"
}
diff --git a/.sqlx/query-4bd5937b38e9ea67215a24f8f4ece05d107b4cdacdb59c30fa3782bb36942b26.json b/.sqlx/query-f48c982a2bf52a2f2de6d70043108ac148363e2f98b301dbeeb1caac330528c5.json
similarity index 83%
rename from .sqlx/query-4bd5937b38e9ea67215a24f8f4ece05d107b4cdacdb59c30fa3782bb36942b26.json
rename to .sqlx/query-f48c982a2bf52a2f2de6d70043108ac148363e2f98b301dbeeb1caac330528c5.json
index 10670b8..720cd39 100644
--- a/.sqlx/query-4bd5937b38e9ea67215a24f8f4ece05d107b4cdacdb59c30fa3782bb36942b26.json
+++ b/.sqlx/query-f48c982a2bf52a2f2de6d70043108ac148363e2f98b301dbeeb1caac330528c5.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT code, pending_identifier, expires_at FROM channel_verifications\n WHERE user_id = $1 AND channel = $2::notification_channel\n ",
+ "query": "\n SELECT code, pending_identifier, expires_at FROM channel_verifications\n WHERE user_id = $1 AND channel = $2::comms_channel\n ",
"describe": {
"columns": [
{
@@ -24,7 +24,7 @@
"Uuid",
{
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -43,5 +43,5 @@
false
]
},
- "hash": "4bd5937b38e9ea67215a24f8f4ece05d107b4cdacdb59c30fa3782bb36942b26"
+ "hash": "f48c982a2bf52a2f2de6d70043108ac148363e2f98b301dbeeb1caac330528c5"
}
diff --git a/.sqlx/query-458c98edc9c01286dc2677fcff82c1f84c5db138fdef9a8e8756771c30b66810.json b/.sqlx/query-f6aede22ec69c30a653b573fed52310cc84faa056f230b0d7ea62a0b457534e0.json
similarity index 57%
rename from .sqlx/query-458c98edc9c01286dc2677fcff82c1f84c5db138fdef9a8e8756771c30b66810.json
rename to .sqlx/query-f6aede22ec69c30a653b573fed52310cc84faa056f230b0d7ea62a0b457534e0.json
index 99dc9cf..5f96327 100644
--- a/.sqlx/query-458c98edc9c01286dc2677fcff82c1f84c5db138fdef9a8e8756771c30b66810.json
+++ b/.sqlx/query-f6aede22ec69c30a653b573fed52310cc84faa056f230b0d7ea62a0b457534e0.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT id, did, email, password_hash, two_factor_enabled,\n preferred_notification_channel as \"preferred_notification_channel: NotificationChannel\",\n deactivated_at, takedown_ref\n FROM users\n WHERE handle = $1 OR email = $1\n ",
+ "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 ",
"describe": {
"columns": [
{
@@ -30,10 +30,10 @@
},
{
"ordinal": 5,
- "name": "preferred_notification_channel: NotificationChannel",
+ "name": "preferred_comms_channel: CommsChannel",
"type_info": {
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -54,6 +54,26 @@
"ordinal": 7,
"name": "takedown_ref",
"type_info": "Text"
+ },
+ {
+ "ordinal": 8,
+ "name": "email_verified",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 9,
+ "name": "discord_verified",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 10,
+ "name": "telegram_verified",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 11,
+ "name": "signal_verified",
+ "type_info": "Bool"
}
],
"parameters": {
@@ -69,8 +89,12 @@
false,
false,
true,
- true
+ true,
+ false,
+ false,
+ false,
+ false
]
},
- "hash": "458c98edc9c01286dc2677fcff82c1f84c5db138fdef9a8e8756771c30b66810"
+ "hash": "f6aede22ec69c30a653b573fed52310cc84faa056f230b0d7ea62a0b457534e0"
}
diff --git a/.sqlx/query-4f131ba30c73a48ddf5630e75f92a86b6ce8a00a4bcae60442d05abc785abc1a.json b/.sqlx/query-fde01bb40898f8a5d45a6e8f89c635c06b4179b5858a7b388404c4b03fc92ab4.json
similarity index 75%
rename from .sqlx/query-4f131ba30c73a48ddf5630e75f92a86b6ce8a00a4bcae60442d05abc785abc1a.json
rename to .sqlx/query-fde01bb40898f8a5d45a6e8f89c635c06b4179b5858a7b388404c4b03fc92ab4.json
index c36e63f..9e183a2 100644
--- a/.sqlx/query-4f131ba30c73a48ddf5630e75f92a86b6ce8a00a4bcae60442d05abc785abc1a.json
+++ b/.sqlx/query-fde01bb40898f8a5d45a6e8f89c635c06b4179b5858a7b388404c4b03fc92ab4.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n SELECT\n created_at,\n channel as \"channel: String\",\n notification_type as \"notification_type: String\",\n status as \"status: String\",\n subject,\n body\n FROM notification_queue\n WHERE user_id = $1\n ORDER BY created_at DESC\n LIMIT 50\n ",
+ "query": "\n SELECT\n created_at,\n channel as \"channel: String\",\n comms_type as \"comms_type: String\",\n status as \"status: String\",\n subject,\n body\n FROM comms_queue\n WHERE user_id = $1\n ORDER BY created_at DESC\n LIMIT 50\n ",
"describe": {
"columns": [
{
@@ -13,7 +13,7 @@
"name": "channel: String",
"type_info": {
"Custom": {
- "name": "notification_channel",
+ "name": "comms_channel",
"kind": {
"Enum": [
"email",
@@ -27,10 +27,10 @@
},
{
"ordinal": 2,
- "name": "notification_type: String",
+ "name": "comms_type: String",
"type_info": {
"Custom": {
- "name": "notification_type",
+ "name": "comms_type",
"kind": {
"Enum": [
"welcome",
@@ -52,7 +52,7 @@
"name": "status: String",
"type_info": {
"Custom": {
- "name": "notification_status",
+ "name": "comms_status",
"kind": {
"Enum": [
"pending",
@@ -89,5 +89,5 @@
false
]
},
- "hash": "4f131ba30c73a48ddf5630e75f92a86b6ce8a00a4bcae60442d05abc785abc1a"
+ "hash": "fde01bb40898f8a5d45a6e8f89c635c06b4179b5858a7b388404c4b03fc92ab4"
}
diff --git a/Cargo.lock b/Cargo.lock
index b48c47d..4b7ba35 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -950,6 +950,7 @@ dependencies = [
"ed25519-dalek",
"futures",
"governor",
+ "hickory-resolver",
"hkdf",
"hmac",
"image",
diff --git a/Cargo.toml b/Cargo.toml
index 0a522ba..8ade4a7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,6 +51,7 @@ iroh-car = "0.5.1"
image = { version = "0.25", default-features = false, features = ["jpeg", "png", "gif", "webp"] }
redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
tower-http = { version = "0.6", features = ["fs", "cors"] }
+hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
metrics = "0.24"
metrics-exporter-prometheus = { version = "0.16", default-features = false, features = ["http-listener"] }
[features]
diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte
index 10dbd23..e207a58 100644
--- a/frontend/src/App.svelte
+++ b/frontend/src/App.svelte
@@ -3,6 +3,7 @@
import { initAuth, getAuthState } from './lib/auth.svelte'
import Login from './routes/Login.svelte'
import Register from './routes/Register.svelte'
+ import Verify from './routes/Verify.svelte'
import ResetPassword from './routes/ResetPassword.svelte'
import Dashboard from './routes/Dashboard.svelte'
import AppPasswords from './routes/AppPasswords.svelte'
@@ -25,6 +26,8 @@
return Login
case '/register':
return Register
+ case '/verify':
+ return Verify
case '/reset-password':
return ResetPassword
case '/dashboard':
diff --git a/frontend/src/routes/Login.svelte b/frontend/src/routes/Login.svelte
index 26f7bbb..127359e 100644
--- a/frontend/src/routes/Login.svelte
+++ b/frontend/src/routes/Login.svelte
@@ -8,11 +8,6 @@
let resendMessage = $state(null)
let showNewLogin = $state(false)
const auth = getAuthState()
- $effect(() => {
- if (auth.session) {
- navigate('/dashboard')
- }
- })
async function handleSwitchAccount(did: string) {
submitting = true
try {
diff --git a/frontend/src/routes/Register.svelte b/frontend/src/routes/Register.svelte
index a8e2334..daa291e 100644
--- a/frontend/src/routes/Register.svelte
+++ b/frontend/src/routes/Register.svelte
@@ -1,7 +1,10 @@
{#if error}
{error}
{/if}
- {#if pendingVerification}
-
Verify Your Account
-
- We've sent a verification code to your {channelLabel(pendingVerification.channel)}.
- Enter it below to complete registration.
-
- {#if resendMessage}
-
{resendMessage}
- {/if}
-
- {:else}
-
Create Account
+
Create Account
Create a new account on this PDS
{#if loadingServerInfo}
Loading...
@@ -322,17 +251,6 @@
required
/>
- {:else}
-
- Invite Code (optional)
-
-
{/if}
{submitting ? 'Creating account...' : 'Create Account'}
@@ -342,7 +260,6 @@
Already have an account? Sign in
{/if}
- {/if}
diff --git a/frontend/src/routes/Verify.svelte b/frontend/src/routes/Verify.svelte
new file mode 100644
index 0000000..9a423ab
--- /dev/null
+++ b/frontend/src/routes/Verify.svelte
@@ -0,0 +1,277 @@
+
+
+
+ {#if error}
+
{error}
+ {/if}
+
+ {#if pendingVerification}
+
Verify Your Account
+
+ We've sent a verification code to your {channelLabel(pendingVerification.channel)}.
+ Enter it below to complete registration.
+
+
Verifying account: @{pendingVerification.handle}
+
+ {#if resendMessage}
+
{resendMessage}
+ {/if}
+
+
+
+
+ clearPendingVerification()}>Start over with a different account
+
+ {:else}
+
Account Verification
+
No pending verification found.
+
+ If you recently created an account and need to verify it, you may need to create a new account.
+ If you already verified your account, you can sign in.
+
+
+ {/if}
+
+
+
diff --git a/migrations/20251219_rename_email_confirmed.sql b/migrations/20251219_rename_email_confirmed.sql
new file mode 100644
index 0000000..5ce3999
--- /dev/null
+++ b/migrations/20251219_rename_email_confirmed.sql
@@ -0,0 +1,6 @@
+DO $$
+BEGIN
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'email_confirmed') THEN
+ ALTER TABLE users RENAME COLUMN email_confirmed TO email_verified;
+ END IF;
+END $$;
diff --git a/migrations/20251220_rename_notifications_to_comms.sql b/migrations/20251220_rename_notifications_to_comms.sql
new file mode 100644
index 0000000..9b9b2f0
--- /dev/null
+++ b/migrations/20251220_rename_notifications_to_comms.sql
@@ -0,0 +1,27 @@
+DO $$
+BEGIN
+ IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'notification_channel') THEN
+ ALTER TYPE notification_channel RENAME TO comms_channel;
+ END IF;
+ IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'notification_status') THEN
+ ALTER TYPE notification_status RENAME TO comms_status;
+ END IF;
+ IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'notification_type') THEN
+ ALTER TYPE notification_type RENAME TO comms_type;
+ END IF;
+ IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'notification_queue') THEN
+ ALTER TABLE notification_queue RENAME TO comms_queue;
+ END IF;
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'comms_queue' AND column_name = 'notification_type') THEN
+ ALTER TABLE comms_queue RENAME COLUMN notification_type TO comms_type;
+ END IF;
+ IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_notification_queue_status_scheduled') THEN
+ ALTER INDEX idx_notification_queue_status_scheduled RENAME TO idx_comms_queue_status_scheduled;
+ END IF;
+ IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_notification_queue_user_id') THEN
+ ALTER INDEX idx_notification_queue_user_id RENAME TO idx_comms_queue_user_id;
+ END IF;
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'preferred_notification_channel') THEN
+ ALTER TABLE users RENAME COLUMN preferred_notification_channel TO preferred_comms_channel;
+ END IF;
+END $$;
diff --git a/src/api/admin/account/email.rs b/src/api/admin/account/email.rs
index d61066d..756d73a 100644
--- a/src/api/admin/account/email.rs
+++ b/src/api/admin/account/email.rs
@@ -87,14 +87,14 @@ pub async fn send_email(
.subject
.clone()
.unwrap_or_else(|| format!("Message from {}", hostname));
- let notification = crate::notifications::NewNotification::email(
+ let item = crate::comms::NewComms::email(
user_id,
- crate::notifications::NotificationType::AdminEmail,
+ crate::comms::CommsType::AdminEmail,
email,
subject,
content.to_string(),
);
- let result = crate::notifications::enqueue_notification(&state.db, notification).await;
+ let result = crate::comms::enqueue_comms(&state.db, item).await;
match result {
Ok(_) => {
tracing::info!("Admin email queued for {} ({})", handle, recipient_did);
diff --git a/src/api/admin/account/info.rs b/src/api/admin/account/info.rs
index b48d7d2..48db175 100644
--- a/src/api/admin/account/info.rs
+++ b/src/api/admin/account/info.rs
@@ -24,7 +24,7 @@ pub struct AccountInfo {
pub indexed_at: String,
pub invite_note: Option,
pub invites_disabled: bool,
- pub email_confirmed_at: Option,
+ pub email_verified_at: Option,
pub deactivated_at: Option,
}
@@ -67,7 +67,7 @@ pub async fn get_account_info(
indexed_at: row.created_at.to_rfc3339(),
invite_note: None,
invites_disabled: false,
- email_confirmed_at: None,
+ email_verified_at: None,
deactivated_at: None,
}),
)
@@ -143,7 +143,7 @@ pub async fn get_account_infos(
indexed_at: row.created_at.to_rfc3339(),
invite_note: None,
invites_disabled: false,
- email_confirmed_at: None,
+ email_verified_at: None,
deactivated_at: None,
});
}
diff --git a/src/api/admin/account/search.rs b/src/api/admin/account/search.rs
index c14d09e..05612ff 100644
--- a/src/api/admin/account/search.rs
+++ b/src/api/admin/account/search.rs
@@ -31,7 +31,7 @@ pub struct AccountView {
pub email: Option,
pub indexed_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
- pub email_confirmed_at: Option,
+ pub email_verified_at: Option,
#[serde(skip_serializing_if = "Option::is_none")]
pub deactivated_at: Option,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -56,7 +56,7 @@ pub async fn search_accounts(
let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h));
let result = sqlx::query_as::<_, (String, String, Option, chrono::DateTime, bool, Option>)>(
r#"
- SELECT did, handle, email, created_at, email_confirmed, deactivated_at
+ SELECT did, handle, email, created_at, email_verified, deactivated_at
FROM users
WHERE did > $1 AND ($2::text IS NULL OR handle ILIKE $2)
ORDER BY did ASC
@@ -74,12 +74,12 @@ pub async fn search_accounts(
let accounts: Vec = rows
.into_iter()
.take(limit as usize)
- .map(|(did, handle, email, created_at, email_confirmed, deactivated_at)| AccountView {
+ .map(|(did, handle, email, created_at, email_verified, deactivated_at)| AccountView {
did: did.clone(),
handle,
email,
indexed_at: created_at.to_rfc3339(),
- email_confirmed_at: if email_confirmed {
+ email_verified_at: if email_verified {
Some(created_at.to_rfc3339())
} else {
None
diff --git a/src/api/identity/account.rs b/src/api/identity/account.rs
index e2eb37b..308bcb0 100644
--- a/src/api/identity/account.rs
+++ b/src/api/identity/account.rs
@@ -322,26 +322,53 @@ pub async fn create_account(
}
Ok(None) => {}
}
- if let Some(code) = &input.invite_code {
- let invite_query = sqlx::query!(
- "SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE",
- code
+ let invite_code_required = std::env::var("INVITE_CODE_REQUIRED")
+ .map(|v| v == "true" || v == "1")
+ .unwrap_or(false);
+ if invite_code_required && input.invite_code.as_ref().map(|c| c.trim().is_empty()).unwrap_or(true) {
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(json!({"error": "InvalidInviteCode", "message": "Invite code is required"})),
)
- .fetch_optional(&mut *tx)
- .await;
- match invite_query {
- Ok(Some(row)) => {
- if row.available_uses <= 0 {
- return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response();
+ .into_response();
+ }
+ if let Some(code) = &input.invite_code {
+ if !code.trim().is_empty() {
+ let invite_query = sqlx::query!(
+ "SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE",
+ code
+ )
+ .fetch_optional(&mut *tx)
+ .await;
+ match invite_query {
+ Ok(Some(row)) => {
+ if row.available_uses <= 0 {
+ return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidInviteCode", "message": "Invite code exhausted"}))).into_response();
+ }
+ let update_invite = sqlx::query!(
+ "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1",
+ code
+ )
+ .execute(&mut *tx)
+ .await;
+ if let Err(e) = update_invite {
+ error!("Error updating invite code: {:?}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": "InternalError"})),
+ )
+ .into_response();
+ }
}
- let update_invite = sqlx::query!(
- "UPDATE invite_codes SET available_uses = available_uses - 1 WHERE code = $1",
- code
- )
- .execute(&mut *tx)
- .await;
- if let Err(e) = update_invite {
- error!("Error updating invite code: {:?}", e);
+ Ok(None) => {
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"})),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ error!("Error checking invite code: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
@@ -349,21 +376,6 @@ pub async fn create_account(
.into_response();
}
}
- Ok(None) => {
- return (
- StatusCode::BAD_REQUEST,
- Json(json!({"error": "InvalidInviteCode", "message": "Invite code not found"})),
- )
- .into_response();
- }
- Err(e) => {
- error!("Error checking invite code: {:?}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(json!({"error": "InternalError"})),
- )
- .into_response();
- }
}
}
let password_hash = match hash(&input.password, DEFAULT_COST) {
@@ -387,10 +399,10 @@ pub async fn create_account(
let user_insert: Result<(uuid::Uuid,), _> = sqlx::query_as(
r#"INSERT INTO users (
handle, email, did, password_hash,
- preferred_notification_channel,
+ preferred_comms_channel,
discord_id, telegram_username, signal_number,
is_admin
- ) VALUES ($1, $2, $3, $4, $5::notification_channel, $6, $7, $8, $9) RETURNING id"#,
+ ) VALUES ($1, $2, $3, $4, $5::comms_channel, $6, $7, $8, $9) RETURNING id"#,
)
.bind(short_handle)
.bind(&email)
@@ -598,20 +610,22 @@ pub async fn create_account(
.into_response();
}
if let Some(code) = &input.invite_code {
- let use_insert = sqlx::query!(
- "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)",
- code,
- user_id
- )
- .execute(&mut *tx)
- .await;
- if let Err(e) = use_insert {
- error!("Error recording invite usage: {:?}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(json!({"error": "InternalError"})),
+ if !code.trim().is_empty() {
+ let use_insert = sqlx::query!(
+ "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)",
+ code,
+ user_id
)
- .into_response();
+ .execute(&mut *tx)
+ .await;
+ if let Err(e) = use_insert {
+ error!("Error recording invite usage: {:?}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({"error": "InternalError"})),
+ )
+ .into_response();
+ }
}
}
if let Err(e) = tx.commit().await {
@@ -646,7 +660,7 @@ pub async fn create_account(
{
warn!("Failed to create default profile for {}: {}", did, e);
}
- if let Err(e) = crate::notifications::enqueue_signup_verification(
+ if let Err(e) = crate::comms::enqueue_signup_verification(
&state.db,
user_id,
verification_channel,
diff --git a/src/api/identity/did.rs b/src/api/identity/did.rs
index 452c6b6..18f386c 100644
--- a/src/api/identity/did.rs
+++ b/src/api/identity/did.rs
@@ -53,11 +53,22 @@ pub async fn resolve_handle(
.await;
(StatusCode::OK, Json(json!({ "did": row.did }))).into_response()
}
- Ok(None) => (
- StatusCode::NOT_FOUND,
- Json(json!({"error": "HandleNotFound", "message": "Unable to resolve handle"})),
- )
- .into_response(),
+ Ok(None) => {
+ match crate::handle::resolve_handle(handle).await {
+ Ok(did) => {
+ let _ = state
+ .cache
+ .set(&cache_key, &did, std::time::Duration::from_secs(300))
+ .await;
+ (StatusCode::OK, Json(json!({ "did": did }))).into_response()
+ }
+ Err(_) => (
+ StatusCode::NOT_FOUND,
+ Json(json!({"error": "HandleNotFound", "message": "Unable to resolve handle"})),
+ )
+ .into_response(),
+ }
+ }
Err(e) => {
error!("DB error resolving handle: {:?}", e);
(
@@ -396,6 +407,54 @@ pub async fn update_handle(
)
.into_response();
}
+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
+ let is_service_domain = crate::handle::is_service_domain_handle(new_handle, &hostname);
+ let (handle_to_store, full_handle) = if is_service_domain {
+ let suffix = format!(".{}", hostname);
+ let short_handle = if new_handle.ends_with(&suffix) {
+ new_handle.strip_suffix(&suffix).unwrap_or(new_handle)
+ } else {
+ new_handle
+ };
+ (short_handle.to_string(), format!("{}.{}", short_handle, hostname))
+ } else {
+ match crate::handle::verify_handle_ownership(new_handle, &did).await {
+ Ok(()) => {}
+ Err(crate::handle::HandleResolutionError::NotFound) => {
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(json!({
+ "error": "HandleNotAvailable",
+ "message": "Handle verification failed. Please set up DNS TXT record at _atproto.{} or serve your DID at https://{}/.well-known/atproto-did",
+ "handle": new_handle
+ })),
+ )
+ .into_response();
+ }
+ Err(crate::handle::HandleResolutionError::DidMismatch { expected, actual }) => {
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(json!({
+ "error": "HandleNotAvailable",
+ "message": format!("Handle points to different DID. Expected {}, got {}", expected, actual)
+ })),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ warn!("Handle verification failed: {}", e);
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(json!({
+ "error": "HandleNotAvailable",
+ "message": format!("Handle verification failed: {}", e)
+ })),
+ )
+ .into_response();
+ }
+ }
+ (new_handle.to_string(), new_handle.to_string())
+ };
let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE id = $1", user_id)
.fetch_optional(&state.db)
.await
@@ -403,7 +462,7 @@ pub async fn update_handle(
.flatten();
let existing = sqlx::query!(
"SELECT id FROM users WHERE handle = $1 AND id != $2",
- new_handle,
+ handle_to_store,
user_id
)
.fetch_optional(&state.db)
@@ -417,7 +476,7 @@ pub async fn update_handle(
}
let result = sqlx::query!(
"UPDATE users SET handle = $1 WHERE id = $2",
- new_handle,
+ handle_to_store,
user_id
)
.execute(&state.db)
@@ -427,16 +486,20 @@ pub async fn update_handle(
if let Some(old) = old_handle {
let _ = state.cache.delete(&format!("handle:{}", old)).await;
}
- let _ = state.cache.delete(&format!("handle:{}", new_handle)).await;
- let hostname =
- std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
- let full_handle = format!("{}.{}", new_handle, hostname);
+ let _ = state
+ .cache
+ .delete(&format!("handle:{}", handle_to_store))
+ .await;
+ let _ = state.cache.delete(&format!("handle:{}", full_handle)).await;
if let Err(e) =
crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle))
.await
{
warn!("Failed to sequence identity event for handle update: {}", e);
}
+ if let Err(e) = update_plc_handle(&state, &did, &full_handle).await {
+ warn!("Failed to update PLC handle: {}", e);
+ }
(StatusCode::OK, Json(json!({}))).into_response()
}
Err(e) => {
@@ -450,6 +513,38 @@ pub async fn update_handle(
}
}
+async fn update_plc_handle(
+ state: &AppState,
+ did: &str,
+ new_handle: &str,
+) -> Result<(), Box> {
+ if !did.starts_with("did:plc:") {
+ return Ok(());
+ }
+ let user_row = sqlx::query!(
+ r#"SELECT u.id, uk.key_bytes, uk.encryption_version
+ FROM users u
+ JOIN user_keys uk ON u.id = uk.user_id
+ WHERE u.did = $1"#,
+ did
+ )
+ .fetch_optional(&state.db)
+ .await?;
+ let user_row = match user_row {
+ Some(r) => r,
+ None => return Ok(()),
+ };
+ let key_bytes = crate::config::decrypt_key(&user_row.key_bytes, user_row.encryption_version)?;
+ let signing_key = k256::ecdsa::SigningKey::from_slice(&key_bytes)?;
+ let plc_client = crate::plc::PlcClient::new(None);
+ let last_op = plc_client.get_last_op(did).await?;
+ let new_also_known_as = vec![format!("at://{}", new_handle)];
+ let update_op = crate::plc::create_update_op(&last_op, None, None, Some(new_also_known_as), None)?;
+ let signed_op = crate::plc::sign_operation(&update_op, &signing_key)?;
+ plc_client.send_operation(did, &signed_op).await?;
+ Ok(())
+}
+
pub async fn well_known_atproto_did(State(state): State, headers: HeaderMap) -> Response {
let host = match headers.get("host").and_then(|h| h.to_str().ok()) {
Some(h) => h,
diff --git a/src/api/identity/plc/request.rs b/src/api/identity/plc/request.rs
index b483cd5..dd4f45c 100644
--- a/src/api/identity/plc/request.rs
+++ b/src/api/identity/plc/request.rs
@@ -68,7 +68,7 @@ pub async fn request_plc_operation_signature(
}
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
if let Err(e) =
- crate::notifications::enqueue_plc_operation(&state.db, user.id, &plc_token, &hostname).await
+ crate::comms::enqueue_plc_operation(&state.db, user.id, &plc_token, &hostname).await
{
warn!("Failed to enqueue PLC operation notification: {:?}", e);
}
diff --git a/src/api/notification_prefs.rs b/src/api/notification_prefs.rs
index 2b40627..989302c 100644
--- a/src/api/notification_prefs.rs
+++ b/src/api/notification_prefs.rs
@@ -60,7 +60,7 @@ pub async fn get_notification_prefs(State(state): State, headers: Head
r#"
SELECT
email,
- preferred_notification_channel::text as channel,
+ preferred_comms_channel::text as channel,
discord_id,
discord_verified,
telegram_username,
@@ -110,7 +110,7 @@ pub async fn get_notification_prefs(State(state): State, headers: Head
pub struct NotificationHistoryEntry {
pub created_at: String,
pub channel: String,
- pub notification_type: String,
+ pub comms_type: String,
pub status: String,
pub subject: Option,
pub body: String,
@@ -164,11 +164,11 @@ pub async fn get_notification_history(
SELECT
created_at,
channel as "channel: String",
- notification_type as "notification_type: String",
+ comms_type as "comms_type: String",
status as "status: String",
subject,
body
- FROM notification_queue
+ FROM comms_queue
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 50
@@ -190,7 +190,7 @@ pub async fn get_notification_history(
NotificationHistoryEntry {
created_at: row.created_at.to_rfc3339(),
channel: row.channel.clone(),
- notification_type: row.notification_type.clone(),
+ comms_type: row.comms_type.clone(),
status: row.status.clone(),
subject: row.subject.clone(),
body: row.body.clone(),
@@ -231,7 +231,7 @@ pub async fn request_channel_verification(
sqlx::query!(
r#"
INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at)
- VALUES ($1, $2::notification_channel, $3, $4, $5)
+ VALUES ($1, $2::comms_channel, $3, $4, $5)
ON CONFLICT (user_id, channel) DO UPDATE
SET code = $3, pending_identifier = $4, expires_at = $5, created_at = NOW()
"#,
@@ -248,14 +248,14 @@ pub async fn request_channel_verification(
if channel == "email" {
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
let handle_str = handle.unwrap_or("user");
- crate::notifications::enqueue_email_update(db, user_id, identifier, handle_str, &code, &hostname)
+ crate::comms::enqueue_email_update(db, user_id, identifier, handle_str, &code, &hostname)
.await
.map_err(|e| format!("Failed to enqueue email notification: {}", e))?;
} else {
sqlx::query!(
r#"
- INSERT INTO notification_queue (user_id, channel, notification_type, recipient, subject, body, metadata)
- VALUES ($1, $2::notification_channel, 'channel_verification', $3, 'Verify your channel', $4, $5)
+ INSERT INTO comms_queue (user_id, channel, comms_type, recipient, subject, body, metadata)
+ VALUES ($1, $2::comms_channel, 'channel_verification', $3, 'Verify your channel', $4, $5)
"#,
user_id,
channel as _,
@@ -331,7 +331,7 @@ pub async fn update_notification_prefs(
.into_response();
}
if let Err(e) = sqlx::query(
- r#"UPDATE users SET preferred_notification_channel = $1::notification_channel, updated_at = NOW() WHERE did = $2"#
+ r#"UPDATE users SET preferred_comms_channel = $1::comms_channel, updated_at = NOW() WHERE did = $2"#
)
.bind(channel)
.bind(&user.did)
diff --git a/src/api/repo/record/batch.rs b/src/api/repo/record/batch.rs
index 2f55fee..43a2295 100644
--- a/src/api/repo/record/batch.rs
+++ b/src/api/repo/record/batch.rs
@@ -1,5 +1,5 @@
use super::validation::validate_record;
-use super::write::has_verified_notification_channel;
+use super::write::has_verified_comms_channel;
use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log};
use crate::repo::tracking::TrackingBlockStore;
use crate::state::AppState;
@@ -109,7 +109,7 @@ pub async fn apply_writes(
)
.into_response();
}
- match has_verified_notification_channel(&state.db, &did).await {
+ match has_verified_comms_channel(&state.db, &did).await {
Ok(true) => {}
Ok(false) => {
return (
diff --git a/src/api/repo/record/write.rs b/src/api/repo/record/write.rs
index a698f2d..fa2b1f6 100644
--- a/src/api/repo/record/write.rs
+++ b/src/api/repo/record/write.rs
@@ -22,14 +22,14 @@ use std::sync::Arc;
use tracing::error;
use uuid::Uuid;
-pub async fn has_verified_notification_channel(
+pub async fn has_verified_comms_channel(
db: &PgPool,
did: &str,
) -> Result {
let row = sqlx::query(
r#"
SELECT
- email_confirmed,
+ email_verified,
discord_verified,
telegram_verified,
signal_verified
@@ -42,11 +42,11 @@ pub async fn has_verified_notification_channel(
.await?;
match row {
Some(r) => {
- let email_confirmed: bool = r.get("email_confirmed");
+ let email_verified: bool = r.get("email_verified");
let discord_verified: bool = r.get("discord_verified");
let telegram_verified: bool = r.get("telegram_verified");
let signal_verified: bool = r.get("signal_verified");
- Ok(email_confirmed || discord_verified || telegram_verified || signal_verified)
+ Ok(email_verified || discord_verified || telegram_verified || signal_verified)
}
None => Ok(false),
}
@@ -96,7 +96,7 @@ pub async fn prepare_repo_write(
)
.into_response());
}
- match has_verified_notification_channel(&state.db, &auth_user.did).await {
+ match has_verified_comms_channel(&state.db, &auth_user.did).await {
Ok(true) => {}
Ok(false) => {
return Err((
diff --git a/src/api/server/account_status.rs b/src/api/server/account_status.rs
index abbefee..b5eb30d 100644
--- a/src/api/server/account_status.rs
+++ b/src/api/server/account_status.rs
@@ -299,7 +299,7 @@ pub async fn request_account_delete(
.into_response();
}
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
- if let Err(e) = crate::notifications::enqueue_account_deletion(
+ if let Err(e) = crate::comms::enqueue_account_deletion(
&state.db,
user_id,
&confirmation_token,
diff --git a/src/api/server/password.rs b/src/api/server/password.rs
index dca17e8..6f623b8 100644
--- a/src/api/server/password.rs
+++ b/src/api/server/password.rs
@@ -100,7 +100,7 @@ pub async fn request_password_reset(
}
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
if let Err(e) =
- crate::notifications::enqueue_password_reset(&state.db, user_id, &code, &hostname).await
+ crate::comms::enqueue_password_reset(&state.db, user_id, &code, &hostname).await
{
warn!("Failed to enqueue password reset notification: {:?}", e);
}
diff --git a/src/api/server/session.rs b/src/api/server/session.rs
index 1f31b31..a698b07 100644
--- a/src/api/server/session.rs
+++ b/src/api/server/session.rs
@@ -35,6 +35,14 @@ fn normalize_handle(identifier: &str, pds_hostname: &str) -> String {
}
}
+fn full_handle(stored_handle: &str, pds_hostname: &str) -> String {
+ if stored_handle.contains('.') {
+ stored_handle.to_string()
+ } else {
+ format!("{}.{}", stored_handle, pds_hostname)
+ }
+}
+
#[derive(Deserialize)]
pub struct CreateSessionInput {
pub identifier: String,
@@ -76,7 +84,7 @@ pub async fn create_session(
let row = match sqlx::query!(
r#"SELECT
u.id, u.did, u.handle, u.password_hash,
- u.email_confirmed, u.discord_verified, u.telegram_verified, u.signal_verified,
+ u.email_verified, u.discord_verified, u.telegram_verified, u.signal_verified,
k.key_bytes, k.encryption_version
FROM users u
JOIN user_keys k ON u.id = k.user_id
@@ -128,7 +136,7 @@ pub async fn create_session(
.into_response();
}
let is_verified =
- row.email_confirmed || row.discord_verified || row.telegram_verified || row.signal_verified;
+ row.email_verified || row.discord_verified || row.telegram_verified || row.signal_verified;
if !is_verified {
warn!("Login attempt for unverified account: {}", row.did);
return (
@@ -169,11 +177,11 @@ pub async fn create_session(
error!("Failed to insert session: {:?}", e);
return ApiError::InternalError.into_response();
}
- let full_handle = format!("{}.{}", row.handle, pds_hostname);
+ let handle = full_handle(&row.handle, &pds_hostname);
Json(CreateSessionOutput {
access_jwt: access_meta.token,
refresh_jwt: refresh_meta.token,
- handle: full_handle,
+ handle,
did: row.did,
})
.into_response()
@@ -185,8 +193,8 @@ pub async fn get_session(
) -> Response {
match sqlx::query!(
r#"SELECT
- handle, email, email_confirmed, is_admin,
- preferred_notification_channel as "preferred_channel: crate::notifications::NotificationChannel",
+ handle, email, email_verified, is_admin,
+ preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
discord_verified, telegram_verified, signal_verified
FROM users WHERE did = $1"#,
auth_user.did
@@ -196,18 +204,18 @@ pub async fn get_session(
{
Ok(Some(row)) => {
let (preferred_channel, preferred_channel_verified) = match row.preferred_channel {
- crate::notifications::NotificationChannel::Email => ("email", row.email_confirmed),
- crate::notifications::NotificationChannel::Discord => ("discord", row.discord_verified),
- crate::notifications::NotificationChannel::Telegram => ("telegram", row.telegram_verified),
- crate::notifications::NotificationChannel::Signal => ("signal", row.signal_verified),
+ crate::comms::CommsChannel::Email => ("email", row.email_verified),
+ crate::comms::CommsChannel::Discord => ("discord", row.discord_verified),
+ crate::comms::CommsChannel::Telegram => ("telegram", row.telegram_verified),
+ crate::comms::CommsChannel::Signal => ("signal", row.signal_verified),
};
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
- let full_handle = format!("{}.{}", row.handle, pds_hostname);
+ let handle = full_handle(&row.handle, &pds_hostname);
Json(json!({
- "handle": full_handle,
+ "handle": handle,
"did": auth_user.did,
"email": row.email,
- "emailConfirmed": row.email_confirmed,
+ "emailVerified": row.email_verified,
"preferredChannel": preferred_channel,
"preferredChannelVerified": preferred_channel_verified,
"isAdmin": row.is_admin,
@@ -407,8 +415,8 @@ pub async fn refresh_session(
}
match sqlx::query!(
r#"SELECT
- handle, email, email_confirmed, is_admin,
- preferred_notification_channel as "preferred_channel: crate::notifications::NotificationChannel",
+ handle, email, email_verified, is_admin,
+ preferred_comms_channel as "preferred_channel: crate::comms::CommsChannel",
discord_verified, telegram_verified, signal_verified
FROM users WHERE did = $1"#,
session_row.did
@@ -418,20 +426,20 @@ pub async fn refresh_session(
{
Ok(Some(u)) => {
let (preferred_channel, preferred_channel_verified) = match u.preferred_channel {
- crate::notifications::NotificationChannel::Email => ("email", u.email_confirmed),
- crate::notifications::NotificationChannel::Discord => ("discord", u.discord_verified),
- crate::notifications::NotificationChannel::Telegram => ("telegram", u.telegram_verified),
- crate::notifications::NotificationChannel::Signal => ("signal", u.signal_verified),
+ crate::comms::CommsChannel::Email => ("email", u.email_verified),
+ crate::comms::CommsChannel::Discord => ("discord", u.discord_verified),
+ crate::comms::CommsChannel::Telegram => ("telegram", u.telegram_verified),
+ crate::comms::CommsChannel::Signal => ("signal", u.signal_verified),
};
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
- let full_handle = format!("{}.{}", u.handle, pds_hostname);
+ let handle = full_handle(&u.handle, &pds_hostname);
Json(json!({
"accessJwt": new_access_meta.token,
"refreshJwt": new_refresh_meta.token,
- "handle": full_handle,
+ "handle": handle,
"did": session_row.did,
"email": u.email,
- "emailConfirmed": u.email_confirmed,
+ "emailVerified": u.email_verified,
"preferredChannel": preferred_channel,
"preferredChannelVerified": preferred_channel_verified,
"isAdmin": u.is_admin,
@@ -464,7 +472,7 @@ pub struct ConfirmSignupOutput {
pub handle: String,
pub did: String,
pub email: Option,
- pub email_confirmed: bool,
+ pub email_verified: bool,
pub preferred_channel: String,
pub preferred_channel_verified: bool,
}
@@ -477,7 +485,7 @@ pub async fn confirm_signup(
let row = match sqlx::query!(
r#"SELECT
u.id, u.did, u.handle, u.email,
- u.preferred_notification_channel as "channel: crate::notifications::NotificationChannel",
+ u.preferred_comms_channel as "channel: crate::comms::CommsChannel",
k.key_bytes, k.encryption_version
FROM users u
JOIN user_keys k ON u.id = k.user_id
@@ -534,10 +542,10 @@ pub async fn confirm_signup(
}
};
let verified_column = match row.channel {
- crate::notifications::NotificationChannel::Email => "email_confirmed",
- crate::notifications::NotificationChannel::Discord => "discord_verified",
- crate::notifications::NotificationChannel::Telegram => "telegram_verified",
- crate::notifications::NotificationChannel::Signal => "signal_verified",
+ crate::comms::CommsChannel::Email => "email_verified",
+ crate::comms::CommsChannel::Discord => "discord_verified",
+ crate::comms::CommsChannel::Telegram => "telegram_verified",
+ crate::comms::CommsChannel::Signal => "signal_verified",
};
let update_query = format!(
"UPDATE users SET {} = TRUE WHERE did = $1",
@@ -590,18 +598,18 @@ pub async fn confirm_signup(
return ApiError::InternalError.into_response();
}
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
- if let Err(e) = crate::notifications::enqueue_welcome(&state.db, row.id, &hostname).await {
+ if let Err(e) = crate::comms::enqueue_welcome(&state.db, row.id, &hostname).await {
warn!("Failed to enqueue welcome notification: {:?}", e);
}
- let email_confirmed = matches!(
+ let email_verified = matches!(
row.channel,
- crate::notifications::NotificationChannel::Email
+ crate::comms::CommsChannel::Email
);
let preferred_channel = match row.channel {
- crate::notifications::NotificationChannel::Email => "email",
- crate::notifications::NotificationChannel::Discord => "discord",
- crate::notifications::NotificationChannel::Telegram => "telegram",
- crate::notifications::NotificationChannel::Signal => "signal",
+ crate::comms::CommsChannel::Email => "email",
+ crate::comms::CommsChannel::Discord => "discord",
+ crate::comms::CommsChannel::Telegram => "telegram",
+ crate::comms::CommsChannel::Signal => "signal",
};
Json(ConfirmSignupOutput {
access_jwt: access_meta.token,
@@ -609,7 +617,7 @@ pub async fn confirm_signup(
handle: row.handle,
did: row.did,
email: row.email,
- email_confirmed,
+ email_verified,
preferred_channel: preferred_channel.to_string(),
preferred_channel_verified: true,
})
@@ -630,9 +638,9 @@ pub async fn resend_verification(
let row = match sqlx::query!(
r#"SELECT
id, handle, email,
- preferred_notification_channel as "channel: crate::notifications::NotificationChannel",
+ preferred_comms_channel as "channel: crate::comms::CommsChannel",
discord_id, telegram_username, signal_number,
- email_confirmed, discord_verified, telegram_verified, signal_verified
+ email_verified, discord_verified, telegram_verified, signal_verified
FROM users
WHERE did = $1"#,
input.did
@@ -650,7 +658,7 @@ pub async fn resend_verification(
}
};
let is_verified =
- row.email_confirmed || row.discord_verified || row.telegram_verified || row.signal_verified;
+ row.email_verified || row.discord_verified || row.telegram_verified || row.signal_verified;
if is_verified {
return ApiError::InvalidRequest("Account is already verified".into()).into_response();
}
@@ -678,20 +686,20 @@ pub async fn resend_verification(
return ApiError::InternalError.into_response();
}
let (channel_str, recipient) = match row.channel {
- crate::notifications::NotificationChannel::Email => {
+ crate::comms::CommsChannel::Email => {
("email", row.email.unwrap_or_default())
}
- crate::notifications::NotificationChannel::Discord => {
+ crate::comms::CommsChannel::Discord => {
("discord", row.discord_id.unwrap_or_default())
}
- crate::notifications::NotificationChannel::Telegram => {
+ crate::comms::CommsChannel::Telegram => {
("telegram", row.telegram_username.unwrap_or_default())
}
- crate::notifications::NotificationChannel::Signal => {
+ crate::comms::CommsChannel::Signal => {
("signal", row.signal_number.unwrap_or_default())
}
};
- if let Err(e) = crate::notifications::enqueue_signup_verification(
+ if let Err(e) = crate::comms::enqueue_signup_verification(
&state.db,
row.id,
channel_str,
diff --git a/src/api/verification.rs b/src/api/verification.rs
index 2bee2c9..7ed9552 100644
--- a/src/api/verification.rs
+++ b/src/api/verification.rs
@@ -68,7 +68,7 @@ pub async fn confirm_channel_verification(
let record = match sqlx::query!(
r#"
SELECT code, pending_identifier, expires_at FROM channel_verifications
- WHERE user_id = $1 AND channel = $2::notification_channel
+ WHERE user_id = $1 AND channel = $2::comms_channel
"#,
user_id,
channel_str as _
@@ -163,7 +163,7 @@ pub async fn confirm_channel_verification(
}
if let Err(e) = sqlx::query!(
- "DELETE FROM channel_verifications WHERE user_id = $1 AND channel = $2::notification_channel",
+ "DELETE FROM channel_verifications WHERE user_id = $1 AND channel = $2::comms_channel",
user_id,
channel_str as _
)
diff --git a/src/comms/mod.rs b/src/comms/mod.rs
new file mode 100644
index 0000000..c14c4be
--- /dev/null
+++ b/src/comms/mod.rs
@@ -0,0 +1,16 @@
+mod sender;
+mod service;
+mod types;
+
+pub use sender::{
+ CommsSender, DiscordSender, EmailSender, SendError, SignalSender, TelegramSender,
+ is_valid_phone_number, sanitize_header_value,
+};
+
+pub use service::{
+ CommsService, channel_display_name, enqueue_2fa_code, enqueue_account_deletion,
+ enqueue_comms, enqueue_email_update, enqueue_email_verification, enqueue_password_reset,
+ enqueue_plc_operation, enqueue_signup_verification, enqueue_welcome,
+};
+
+pub use types::{CommsChannel, CommsStatus, CommsType, NewComms, QueuedComms};
diff --git a/src/notifications/sender.rs b/src/comms/sender.rs
similarity index 89%
rename from src/notifications/sender.rs
rename to src/comms/sender.rs
index 4a2b4ff..c2b9434 100644
--- a/src/notifications/sender.rs
+++ b/src/comms/sender.rs
@@ -6,16 +6,16 @@ use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
-use super::types::{NotificationChannel, QueuedNotification};
+use super::types::{CommsChannel, QueuedComms};
const HTTP_TIMEOUT_SECS: u64 = 30;
const MAX_RETRIES: u32 = 3;
const INITIAL_RETRY_DELAY_MS: u64 = 500;
#[async_trait]
-pub trait NotificationSender: Send + Sync {
- fn channel(&self) -> NotificationChannel;
- async fn send(&self, notification: &QueuedNotification) -> Result<(), SendError>;
+pub trait CommsSender: Send + Sync {
+ fn channel(&self) -> CommsChannel;
+ async fn send(&self, notification: &QueuedComms) -> Result<(), SendError>;
}
#[derive(Debug, thiserror::Error)]
@@ -25,7 +25,7 @@ pub enum SendError {
#[error("Sendmail exited with non-zero status: {0}")]
SendmailFailed(String),
#[error("Channel not configured: {0:?}")]
- NotConfigured(NotificationChannel),
+ NotConfigured(CommsChannel),
#[error("External service error: {0}")]
ExternalService(String),
#[error("Invalid recipient format: {0}")]
@@ -91,7 +91,7 @@ impl EmailSender {
Some(Self::new(from_address, from_name))
}
- pub fn format_email(&self, notification: &QueuedNotification) -> String {
+ pub fn format_email(&self, notification: &QueuedComms) -> String {
let subject =
sanitize_header_value(notification.subject.as_deref().unwrap_or("Notification"));
let recipient = sanitize_header_value(¬ification.recipient);
@@ -112,12 +112,12 @@ impl EmailSender {
}
#[async_trait]
-impl NotificationSender for EmailSender {
- fn channel(&self) -> NotificationChannel {
- NotificationChannel::Email
+impl CommsSender for EmailSender {
+ fn channel(&self) -> CommsChannel {
+ CommsChannel::Email
}
- async fn send(&self, notification: &QueuedNotification) -> Result<(), SendError> {
+ async fn send(&self, notification: &QueuedComms) -> Result<(), SendError> {
let email_content = self.format_email(notification);
let mut child = Command::new(&self.sendmail_path)
.arg("-t")
@@ -158,12 +158,12 @@ impl DiscordSender {
}
#[async_trait]
-impl NotificationSender for DiscordSender {
- fn channel(&self) -> NotificationChannel {
- NotificationChannel::Discord
+impl CommsSender for DiscordSender {
+ fn channel(&self) -> CommsChannel {
+ CommsChannel::Discord
}
- async fn send(&self, notification: &QueuedNotification) -> Result<(), SendError> {
+ async fn send(&self, notification: &QueuedComms) -> Result<(), SendError> {
let subject = notification.subject.as_deref().unwrap_or("Notification");
let content = format!("**{}**\n\n{}", subject, notification.body);
let payload = json!({
@@ -237,12 +237,12 @@ impl TelegramSender {
}
#[async_trait]
-impl NotificationSender for TelegramSender {
- fn channel(&self) -> NotificationChannel {
- NotificationChannel::Telegram
+impl CommsSender for TelegramSender {
+ fn channel(&self) -> CommsChannel {
+ CommsChannel::Telegram
}
- async fn send(&self, notification: &QueuedNotification) -> Result<(), SendError> {
+ async fn send(&self, notification: &QueuedComms) -> Result<(), SendError> {
let chat_id = ¬ification.recipient;
let subject = notification.subject.as_deref().unwrap_or("Notification");
let text = format!("*{}*\n\n{}", subject, notification.body);
@@ -316,12 +316,12 @@ impl SignalSender {
}
#[async_trait]
-impl NotificationSender for SignalSender {
- fn channel(&self) -> NotificationChannel {
- NotificationChannel::Signal
+impl CommsSender for SignalSender {
+ fn channel(&self) -> CommsChannel {
+ CommsChannel::Signal
}
- async fn send(&self, notification: &QueuedNotification) -> Result<(), SendError> {
+ async fn send(&self, notification: &QueuedComms) -> Result<(), SendError> {
let recipient = ¬ification.recipient;
if !is_valid_phone_number(recipient) {
return Err(SendError::InvalidRecipient(format!(
diff --git a/src/notifications/service.rs b/src/comms/service.rs
similarity index 63%
rename from src/notifications/service.rs
rename to src/comms/service.rs
index de62eed..3a75585 100644
--- a/src/notifications/service.rs
+++ b/src/comms/service.rs
@@ -9,17 +9,17 @@ use tokio::time::interval;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
-use super::sender::{NotificationSender, SendError};
-use super::types::{NewNotification, NotificationChannel, NotificationStatus, QueuedNotification};
+use super::sender::{CommsSender, SendError};
+use super::types::{NewComms, CommsChannel, CommsStatus, QueuedComms};
-pub struct NotificationService {
+pub struct CommsService {
db: PgPool,
- senders: HashMap>,
+ senders: HashMap>,
poll_interval: Duration,
batch_size: i64,
}
-impl NotificationService {
+impl CommsService {
pub fn new(db: PgPool) -> Self {
let poll_interval_ms: u64 = std::env::var("NOTIFICATION_POLL_INTERVAL_MS")
.ok()
@@ -47,30 +47,30 @@ impl NotificationService {
self
}
- pub fn register_sender(mut self, sender: S) -> Self {
+ pub fn register_sender(mut self, sender: S) -> Self {
self.senders.insert(sender.channel(), Arc::new(sender));
self
}
- pub async fn enqueue(&self, notification: NewNotification) -> Result {
+ pub async fn enqueue(&self, item: NewComms) -> Result {
let id = sqlx::query_scalar!(
r#"
- INSERT INTO notification_queue
- (user_id, channel, notification_type, recipient, subject, body, metadata)
+ INSERT INTO comms_queue
+ (user_id, channel, comms_type, recipient, subject, body, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id
"#,
- notification.user_id,
- notification.channel as NotificationChannel,
- notification.notification_type as super::types::NotificationType,
- notification.recipient,
- notification.subject,
- notification.body,
- notification.metadata
+ item.user_id,
+ item.channel as CommsChannel,
+ item.comms_type as super::types::CommsType,
+ item.recipient,
+ item.subject,
+ item.body,
+ item.metadata
)
.fetch_one(&self.db)
.await?;
- debug!(notification_id = %id, "Notification enqueued");
+ debug!(comms_id = %id, "Comms enqueued");
Ok(id)
}
@@ -81,26 +81,26 @@ impl NotificationService {
pub async fn run(self, mut shutdown: watch::Receiver) {
if self.senders.is_empty() {
warn!(
- "Notification service starting with no senders configured. Notifications will be queued but not delivered until senders are configured."
+ "Comms service starting with no senders configured. Messages will be queued but not delivered until senders are configured."
);
}
info!(
poll_interval_secs = self.poll_interval.as_secs(),
batch_size = self.batch_size,
channels = ?self.senders.keys().collect::>(),
- "Starting notification service"
+ "Starting comms service"
);
let mut ticker = interval(self.poll_interval);
loop {
tokio::select! {
_ = ticker.tick() => {
if let Err(e) = self.process_batch().await {
- error!(error = %e, "Failed to process notification batch");
+ error!(error = %e, "Failed to process comms batch");
}
}
_ = shutdown.changed() => {
if *shutdown.borrow() {
- info!("Notification service shutting down");
+ info!("Comms service shutting down");
break;
}
}
@@ -109,26 +109,26 @@ impl NotificationService {
}
async fn process_batch(&self) -> Result<(), sqlx::Error> {
- let notifications = self.fetch_pending_notifications().await?;
- if notifications.is_empty() {
+ let items = self.fetch_pending().await?;
+ if items.is_empty() {
return Ok(());
}
- debug!(count = notifications.len(), "Processing notification batch");
- for notification in notifications {
- self.process_notification(notification).await;
+ debug!(count = items.len(), "Processing comms batch");
+ for item in items {
+ self.process_item(item).await;
}
Ok(())
}
- async fn fetch_pending_notifications(&self) -> Result, sqlx::Error> {
+ async fn fetch_pending(&self) -> Result, sqlx::Error> {
let now = Utc::now();
sqlx::query_as!(
- QueuedNotification,
+ QueuedComms,
r#"
- UPDATE notification_queue
+ UPDATE comms_queue
SET status = 'processing', updated_at = NOW()
WHERE id IN (
- SELECT id FROM notification_queue
+ SELECT id FROM comms_queue
WHERE status = 'pending'
AND scheduled_for <= $1
AND attempts < max_attempts
@@ -138,9 +138,9 @@ impl NotificationService {
)
RETURNING
id, user_id,
- channel as "channel: NotificationChannel",
- notification_type as "notification_type: super::types::NotificationType",
- status as "status: NotificationStatus",
+ channel as "channel: CommsChannel",
+ comms_type as "comms_type: super::types::CommsType",
+ status as "status: CommsStatus",
recipient, subject, body, metadata,
attempts, max_attempts, last_error,
created_at, updated_at, scheduled_for, processed_at
@@ -152,14 +152,14 @@ impl NotificationService {
.await
}
- async fn process_notification(&self, notification: QueuedNotification) {
- let notification_id = notification.id;
- let channel = notification.channel;
+ async fn process_item(&self, item: QueuedComms) {
+ let comms_id = item.id;
+ let channel = item.channel;
let result = match self.senders.get(&channel) {
- Some(sender) => sender.send(¬ification).await,
+ Some(sender) => sender.send(&item).await,
None => {
warn!(
- notification_id = %notification_id,
+ comms_id = %comms_id,
channel = ?channel,
"No sender registered for channel"
);
@@ -168,27 +168,27 @@ impl NotificationService {
};
match result {
Ok(()) => {
- debug!(notification_id = %notification_id, "Notification sent successfully");
- if let Err(e) = self.mark_sent(notification_id).await {
+ debug!(comms_id = %comms_id, "Comms sent successfully");
+ if let Err(e) = self.mark_sent(comms_id).await {
error!(
- notification_id = %notification_id,
+ comms_id = %comms_id,
error = %e,
- "Failed to mark notification as sent"
+ "Failed to mark comms as sent"
);
}
}
Err(e) => {
let error_msg = e.to_string();
warn!(
- notification_id = %notification_id,
+ comms_id = %comms_id,
error = %error_msg,
- "Failed to send notification"
+ "Failed to send comms"
);
- if let Err(db_err) = self.mark_failed(notification_id, &error_msg).await {
+ if let Err(db_err) = self.mark_failed(comms_id, &error_msg).await {
error!(
- notification_id = %notification_id,
+ comms_id = %comms_id,
error = %db_err,
- "Failed to mark notification as failed"
+ "Failed to mark comms as failed"
);
}
}
@@ -198,7 +198,7 @@ impl NotificationService {
async fn mark_sent(&self, id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
- UPDATE notification_queue
+ UPDATE comms_queue
SET status = 'sent', processed_at = NOW(), updated_at = NOW()
WHERE id = $1
"#,
@@ -212,11 +212,11 @@ impl NotificationService {
async fn mark_failed(&self, id: Uuid, error: &str) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
- UPDATE notification_queue
+ UPDATE comms_queue
SET
status = CASE
- WHEN attempts + 1 >= max_attempts THEN 'failed'::notification_status
- ELSE 'pending'::notification_status
+ WHEN attempts + 1 >= max_attempts THEN 'failed'::comms_status
+ ELSE 'pending'::comms_status
END,
attempts = attempts + 1,
last_error = $2,
@@ -233,45 +233,42 @@ impl NotificationService {
}
}
-pub async fn enqueue_notification(
- db: &PgPool,
- notification: NewNotification,
-) -> Result {
+pub async fn enqueue_comms(db: &PgPool, item: NewComms) -> Result {
sqlx::query_scalar!(
r#"
- INSERT INTO notification_queue
- (user_id, channel, notification_type, recipient, subject, body, metadata)
+ INSERT INTO comms_queue
+ (user_id, channel, comms_type, recipient, subject, body, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id
"#,
- notification.user_id,
- notification.channel as NotificationChannel,
- notification.notification_type as super::types::NotificationType,
- notification.recipient,
- notification.subject,
- notification.body,
- notification.metadata
+ item.user_id,
+ item.channel as CommsChannel,
+ item.comms_type as super::types::CommsType,
+ item.recipient,
+ item.subject,
+ item.body,
+ item.metadata
)
.fetch_one(db)
.await
}
-pub struct UserNotificationPrefs {
- pub channel: NotificationChannel,
+pub struct UserCommsPrefs {
+ pub channel: CommsChannel,
pub email: Option,
pub handle: String,
}
-pub async fn get_user_notification_prefs(
+pub async fn get_user_comms_prefs(
db: &PgPool,
user_id: Uuid,
-) -> Result {
+) -> Result {
let row = sqlx::query!(
r#"
SELECT
email,
handle,
- preferred_notification_channel as "channel: NotificationChannel"
+ preferred_comms_channel as "channel: CommsChannel"
FROM users
WHERE id = $1
"#,
@@ -279,7 +276,7 @@ pub async fn get_user_notification_prefs(
)
.fetch_one(db)
.await?;
- Ok(UserNotificationPrefs {
+ Ok(UserCommsPrefs {
channel: row.channel,
email: row.email,
handle: row.handle,
@@ -291,17 +288,17 @@ pub async fn enqueue_welcome(
user_id: Uuid,
hostname: &str,
) -> Result {
- let prefs = get_user_notification_prefs(db, user_id).await?;
+ let prefs = get_user_comms_prefs(db, user_id).await?;
let body = format!(
"Welcome to {}!\n\nYour handle is: @{}\n\nThank you for joining us.",
hostname, prefs.handle
);
- enqueue_notification(
+ enqueue_comms(
db,
- NewNotification::new(
+ NewComms::new(
user_id,
prefs.channel,
- super::types::NotificationType::Welcome,
+ super::types::CommsType::Welcome,
prefs.email.clone().unwrap_or_default(),
Some(format!("Welcome to {}", hostname)),
body,
@@ -322,11 +319,11 @@ pub async fn enqueue_email_verification(
"Hello @{},\n\nYour email verification code is: {}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please ignore this email.",
handle, code
);
- enqueue_notification(
+ enqueue_comms(
db,
- NewNotification::email(
+ NewComms::email(
user_id,
- super::types::NotificationType::EmailVerification,
+ super::types::CommsType::EmailVerification,
email.to_string(),
format!("Verify your email - {}", hostname),
body,
@@ -341,17 +338,17 @@ pub async fn enqueue_password_reset(
code: &str,
hostname: &str,
) -> Result {
- let prefs = get_user_notification_prefs(db, user_id).await?;
+ let prefs = get_user_comms_prefs(db, user_id).await?;
let body = format!(
"Hello @{},\n\nYour password reset code is: {}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please ignore this message.",
prefs.handle, code
);
- enqueue_notification(
+ enqueue_comms(
db,
- NewNotification::new(
+ NewComms::new(
user_id,
prefs.channel,
- super::types::NotificationType::PasswordReset,
+ super::types::CommsType::PasswordReset,
prefs.email.clone().unwrap_or_default(),
Some(format!("Password Reset - {}", hostname)),
body,
@@ -372,11 +369,11 @@ pub async fn enqueue_email_update(
"Hello @{},\n\nYour email update confirmation code is: {}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please ignore this email.",
handle, code
);
- enqueue_notification(
+ enqueue_comms(
db,
- NewNotification::email(
+ NewComms::email(
user_id,
- super::types::NotificationType::EmailUpdate,
+ super::types::CommsType::EmailUpdate,
new_email.to_string(),
format!("Confirm your new email - {}", hostname),
body,
@@ -391,17 +388,17 @@ pub async fn enqueue_account_deletion(
code: &str,
hostname: &str,
) -> Result {
- let prefs = get_user_notification_prefs(db, user_id).await?;
+ let prefs = get_user_comms_prefs(db, user_id).await?;
let body = format!(
"Hello @{},\n\nYour account deletion confirmation code is: {}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please secure your account immediately.",
prefs.handle, code
);
- enqueue_notification(
+ enqueue_comms(
db,
- NewNotification::new(
+ NewComms::new(
user_id,
prefs.channel,
- super::types::NotificationType::AccountDeletion,
+ super::types::CommsType::AccountDeletion,
prefs.email.clone().unwrap_or_default(),
Some(format!("Account Deletion Request - {}", hostname)),
body,
@@ -416,17 +413,17 @@ pub async fn enqueue_plc_operation(
token: &str,
hostname: &str,
) -> Result {
- let prefs = get_user_notification_prefs(db, user_id).await?;
+ let prefs = get_user_comms_prefs(db, user_id).await?;
let body = format!(
"Hello @{},\n\nYou requested to sign a PLC operation for your account.\n\nYour verification token is: {}\n\nThis token will expire in 10 minutes.\n\nIf you did not request this, you can safely ignore this message.",
prefs.handle, token
);
- enqueue_notification(
+ enqueue_comms(
db,
- NewNotification::new(
+ NewComms::new(
user_id,
prefs.channel,
- super::types::NotificationType::PlcOperation,
+ super::types::CommsType::PlcOperation,
prefs.email.clone().unwrap_or_default(),
Some(format!("{} - PLC Operation Token", hostname)),
body,
@@ -441,17 +438,17 @@ pub async fn enqueue_2fa_code(
code: &str,
hostname: &str,
) -> Result {
- let prefs = get_user_notification_prefs(db, user_id).await?;
+ let prefs = get_user_comms_prefs(db, user_id).await?;
let body = format!(
"Hello @{},\n\nYour sign-in verification code is: {}\n\nThis code will expire in 10 minutes.\n\nIf you did not request this, please secure your account immediately.",
prefs.handle, code
);
- enqueue_notification(
+ enqueue_comms(
db,
- NewNotification::new(
+ NewComms::new(
user_id,
prefs.channel,
- super::types::NotificationType::TwoFactorCode,
+ super::types::CommsType::TwoFactorCode,
prefs.email.clone().unwrap_or_default(),
Some(format!("Sign-in Verification - {}", hostname)),
body,
@@ -460,12 +457,12 @@ pub async fn enqueue_2fa_code(
.await
}
-pub fn channel_display_name(channel: NotificationChannel) -> &'static str {
+pub fn channel_display_name(channel: CommsChannel) -> &'static str {
match channel {
- NotificationChannel::Email => "email",
- NotificationChannel::Discord => "Discord",
- NotificationChannel::Telegram => "Telegram",
- NotificationChannel::Signal => "Signal",
+ CommsChannel::Email => "email",
+ CommsChannel::Discord => "Discord",
+ CommsChannel::Telegram => "Telegram",
+ CommsChannel::Signal => "Signal",
}
}
@@ -477,27 +474,27 @@ pub async fn enqueue_signup_verification(
code: &str,
) -> Result {
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
- let notification_channel = match channel {
- "email" => NotificationChannel::Email,
- "discord" => NotificationChannel::Discord,
- "telegram" => NotificationChannel::Telegram,
- "signal" => NotificationChannel::Signal,
- _ => NotificationChannel::Email,
+ let comms_channel = match channel {
+ "email" => CommsChannel::Email,
+ "discord" => CommsChannel::Discord,
+ "telegram" => CommsChannel::Telegram,
+ "signal" => CommsChannel::Signal,
+ _ => CommsChannel::Email,
};
let body = format!(
"Welcome! Your account verification code is: {}\n\nThis code will expire in 30 minutes.\n\nEnter this code to complete your registration on {}.",
code, hostname
);
- let subject = match notification_channel {
- NotificationChannel::Email => Some(format!("Verify your account - {}", hostname)),
+ let subject = match comms_channel {
+ CommsChannel::Email => Some(format!("Verify your account - {}", hostname)),
_ => None,
};
- enqueue_notification(
+ enqueue_comms(
db,
- NewNotification::new(
+ NewComms::new(
user_id,
- notification_channel,
- super::types::NotificationType::EmailVerification,
+ comms_channel,
+ super::types::CommsType::EmailVerification,
recipient.to_string(),
subject,
body,
diff --git a/src/notifications/types.rs b/src/comms/types.rs
similarity index 68%
rename from src/notifications/types.rs
rename to src/comms/types.rs
index f8eacb3..4c3d6e2 100644
--- a/src/notifications/types.rs
+++ b/src/comms/types.rs
@@ -4,8 +4,8 @@ use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type, Serialize, Deserialize)]
-#[sqlx(type_name = "notification_channel", rename_all = "lowercase")]
-pub enum NotificationChannel {
+#[sqlx(type_name = "comms_channel", rename_all = "lowercase")]
+pub enum CommsChannel {
Email,
Discord,
Telegram,
@@ -13,8 +13,8 @@ pub enum NotificationChannel {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)]
-#[sqlx(type_name = "notification_status", rename_all = "lowercase")]
-pub enum NotificationStatus {
+#[sqlx(type_name = "comms_status", rename_all = "lowercase")]
+pub enum CommsStatus {
Pending,
Processing,
Sent,
@@ -22,8 +22,8 @@ pub enum NotificationStatus {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, Serialize, Deserialize)]
-#[sqlx(type_name = "notification_type", rename_all = "snake_case")]
-pub enum NotificationType {
+#[sqlx(type_name = "comms_type", rename_all = "snake_case")]
+pub enum CommsType {
Welcome,
EmailVerification,
PasswordReset,
@@ -35,12 +35,12 @@ pub enum NotificationType {
}
#[derive(Debug, Clone, FromRow)]
-pub struct QueuedNotification {
+pub struct QueuedComms {
pub id: Uuid,
pub user_id: Uuid,
- pub channel: NotificationChannel,
- pub notification_type: NotificationType,
- pub status: NotificationStatus,
+ pub channel: CommsChannel,
+ pub comms_type: CommsType,
+ pub status: CommsStatus,
pub recipient: String,
pub subject: Option,
pub body: String,
@@ -54,21 +54,21 @@ pub struct QueuedNotification {
pub processed_at: Option>,
}
-pub struct NewNotification {
+pub struct NewComms {
pub user_id: Uuid,
- pub channel: NotificationChannel,
- pub notification_type: NotificationType,
+ pub channel: CommsChannel,
+ pub comms_type: CommsType,
pub recipient: String,
pub subject: Option,
pub body: String,
pub metadata: Option,
}
-impl NewNotification {
+impl NewComms {
pub fn new(
user_id: Uuid,
- channel: NotificationChannel,
- notification_type: NotificationType,
+ channel: CommsChannel,
+ comms_type: CommsType,
recipient: String,
subject: Option,
body: String,
@@ -76,7 +76,7 @@ impl NewNotification {
Self {
user_id,
channel,
- notification_type,
+ comms_type,
recipient,
subject,
body,
@@ -86,15 +86,15 @@ impl NewNotification {
pub fn email(
user_id: Uuid,
- notification_type: NotificationType,
+ comms_type: CommsType,
recipient: String,
subject: String,
body: String,
) -> Self {
Self::new(
user_id,
- NotificationChannel::Email,
- notification_type,
+ CommsChannel::Email,
+ comms_type,
recipient,
Some(subject),
body,
diff --git a/src/handle/mod.rs b/src/handle/mod.rs
new file mode 100644
index 0000000..31cb09a
--- /dev/null
+++ b/src/handle/mod.rs
@@ -0,0 +1,121 @@
+use hickory_resolver::config::{ResolverConfig, ResolverOpts};
+use hickory_resolver::TokioAsyncResolver;
+use reqwest::Client;
+use std::time::Duration;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum HandleResolutionError {
+ #[error("DNS lookup failed: {0}")]
+ DnsError(String),
+ #[error("HTTP request failed: {0}")]
+ HttpError(String),
+ #[error("No DID found for handle")]
+ NotFound,
+ #[error("Invalid DID format in record")]
+ InvalidDid,
+ #[error("DID mismatch: expected {expected}, got {actual}")]
+ DidMismatch { expected: String, actual: String },
+}
+
+pub async fn resolve_handle_dns(handle: &str) -> Result {
+ let resolver = TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
+ let query_name = format!("_atproto.{}", handle);
+ let txt_lookup = resolver
+ .txt_lookup(&query_name)
+ .await
+ .map_err(|e| HandleResolutionError::DnsError(e.to_string()))?;
+ for record in txt_lookup.iter() {
+ for txt in record.txt_data() {
+ let txt_str = String::from_utf8_lossy(txt);
+ if let Some(did) = txt_str.strip_prefix("did=") {
+ let did = did.trim();
+ if did.starts_with("did:") {
+ return Ok(did.to_string());
+ }
+ }
+ }
+ }
+ Err(HandleResolutionError::NotFound)
+}
+
+pub async fn resolve_handle_http(handle: &str) -> Result {
+ let url = format!("https://{}/.well-known/atproto-did", handle);
+ let client = Client::builder()
+ .timeout(Duration::from_secs(10))
+ .redirect(reqwest::redirect::Policy::limited(5))
+ .build()
+ .map_err(|e| HandleResolutionError::HttpError(e.to_string()))?;
+ let response = client
+ .get(&url)
+ .header("Accept", "text/plain")
+ .send()
+ .await
+ .map_err(|e| HandleResolutionError::HttpError(e.to_string()))?;
+ if !response.status().is_success() {
+ return Err(HandleResolutionError::NotFound);
+ }
+ let body = response
+ .text()
+ .await
+ .map_err(|e| HandleResolutionError::HttpError(e.to_string()))?;
+ let did = body.trim();
+ if did.starts_with("did:") {
+ Ok(did.to_string())
+ } else {
+ Err(HandleResolutionError::InvalidDid)
+ }
+}
+
+pub async fn resolve_handle(handle: &str) -> Result {
+ match resolve_handle_dns(handle).await {
+ Ok(did) => return Ok(did),
+ Err(e) => {
+ tracing::debug!("DNS resolution failed for {}: {}, trying HTTP", handle, e);
+ }
+ }
+ resolve_handle_http(handle).await
+}
+
+pub async fn verify_handle_ownership(
+ handle: &str,
+ expected_did: &str,
+) -> Result<(), HandleResolutionError> {
+ let resolved_did = resolve_handle(handle).await?;
+ if resolved_did == expected_did {
+ Ok(())
+ } else {
+ Err(HandleResolutionError::DidMismatch {
+ expected: expected_did.to_string(),
+ actual: resolved_did,
+ })
+ }
+}
+
+pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool {
+ let service_domains: Vec = std::env::var("PDS_SERVICE_HANDLE_DOMAINS")
+ .map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
+ .unwrap_or_else(|_| vec![hostname.to_string()]);
+ for domain in service_domains {
+ if handle.ends_with(&format!(".{}", domain)) {
+ return true;
+ }
+ if handle == domain {
+ return true;
+ }
+ }
+ false
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_is_service_domain_handle() {
+ assert!(is_service_domain_handle("user.example.com", "example.com"));
+ assert!(is_service_domain_handle("example.com", "example.com"));
+ assert!(!is_service_domain_handle("user.other.com", "example.com"));
+ assert!(!is_service_domain_handle("myhandle.xyz", "example.com"));
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 3f2c2f7..81e200b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,9 +5,10 @@ pub mod cache;
pub mod circuit_breaker;
pub mod config;
pub mod crawlers;
+pub mod handle;
pub mod image;
pub mod metrics;
-pub mod notifications;
+pub mod comms;
pub mod oauth;
pub mod plc;
pub mod rate_limit;
diff --git a/src/main.rs b/src/main.rs
index d9dc231..1a15ef6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,5 @@
+use bspds::comms::{CommsService, DiscordSender, EmailSender, SignalSender, TelegramSender};
use bspds::crawlers::{Crawlers, start_crawlers_service};
-use bspds::notifications::{
- DiscordSender, EmailSender, NotificationService, SignalSender, TelegramSender,
-};
use bspds::state::AppState;
use std::net::SocketAddr;
use std::process::ExitCode;
@@ -68,31 +66,31 @@ async fn run() -> Result<(), Box> {
let (shutdown_tx, shutdown_rx) = watch::channel(false);
- let mut notification_service = NotificationService::new(pool);
+ let mut comms_service = CommsService::new(pool);
if let Some(email_sender) = EmailSender::from_env() {
- info!("Email notifications enabled");
- notification_service = notification_service.register_sender(email_sender);
+ info!("Email comms enabled");
+ comms_service = comms_service.register_sender(email_sender);
} else {
- warn!("Email notifications disabled (MAIL_FROM_ADDRESS not set)");
+ warn!("Email comms disabled (MAIL_FROM_ADDRESS not set)");
}
if let Some(discord_sender) = DiscordSender::from_env() {
- info!("Discord notifications enabled");
- notification_service = notification_service.register_sender(discord_sender);
+ info!("Discord comms enabled");
+ comms_service = comms_service.register_sender(discord_sender);
}
if let Some(telegram_sender) = TelegramSender::from_env() {
- info!("Telegram notifications enabled");
- notification_service = notification_service.register_sender(telegram_sender);
+ info!("Telegram comms enabled");
+ comms_service = comms_service.register_sender(telegram_sender);
}
if let Some(signal_sender) = SignalSender::from_env() {
- info!("Signal notifications enabled");
- notification_service = notification_service.register_sender(signal_sender);
+ info!("Signal comms enabled");
+ comms_service = comms_service.register_sender(signal_sender);
}
- let notification_handle = tokio::spawn(notification_service.run(shutdown_rx.clone()));
+ let comms_handle = tokio::spawn(comms_service.run(shutdown_rx.clone()));
let crawlers_handle = if let Some(crawlers) = Crawlers::from_env() {
let crawlers = Arc::new(
@@ -122,7 +120,7 @@ async fn run() -> Result<(), Box> {
.with_graceful_shutdown(shutdown_signal(shutdown_tx))
.await;
- notification_handle.await.ok();
+ comms_handle.await.ok();
if let Some(handle) = crawlers_handle {
handle.await.ok();
diff --git a/src/metrics.rs b/src/metrics.rs
index a8a0850..2945888 100644
--- a/src/metrics.rs
+++ b/src/metrics.rs
@@ -54,8 +54,8 @@ fn describe_metrics() {
"Total number of S3/blob storage operations"
);
metrics::describe_gauge!(
- "bspds_notification_queue_size",
- "Current size of the notification queue"
+ "bspds_comms_queue_size",
+ "Current size of the comms queue"
);
metrics::describe_counter!(
"bspds_rate_limit_rejections_total",
@@ -167,8 +167,8 @@ pub fn record_s3_operation(op_type: &str, status: &str) {
.increment(1);
}
-pub fn set_notification_queue_size(size: usize) {
- gauge!("bspds_notification_queue_size").set(size as f64);
+pub fn set_comms_queue_size(size: usize) {
+ gauge!("bspds_comms_queue_size").set(size as f64);
}
pub fn record_rate_limit_rejection(limiter: &str) {
diff --git a/src/notifications/mod.rs b/src/notifications/mod.rs
deleted file mode 100644
index df25f31..0000000
--- a/src/notifications/mod.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-mod sender;
-mod service;
-mod types;
-
-pub use sender::{
- DiscordSender, EmailSender, NotificationSender, SendError, SignalSender, TelegramSender,
- is_valid_phone_number, sanitize_header_value,
-};
-
-pub use service::{
- NotificationService, channel_display_name, enqueue_2fa_code, enqueue_account_deletion,
- enqueue_email_update, enqueue_email_verification, enqueue_notification, enqueue_password_reset,
- enqueue_plc_operation, enqueue_signup_verification, enqueue_welcome,
-};
-
-pub use types::{
- NewNotification, NotificationChannel, NotificationStatus, NotificationType, QueuedNotification,
-};
diff --git a/src/oauth/endpoints/authorize.rs b/src/oauth/endpoints/authorize.rs
index d731f8d..60d0698 100644
--- a/src/oauth/endpoints/authorize.rs
+++ b/src/oauth/endpoints/authorize.rs
@@ -1,4 +1,4 @@
-use crate::notifications::{NotificationChannel, channel_display_name, enqueue_2fa_code};
+use crate::comms::{CommsChannel, channel_display_name, enqueue_2fa_code};
use crate::oauth::{
Code, DeviceAccount, DeviceData, DeviceId, OAuthError, SessionId, client::ClientMetadataCache, db, templates,
};
@@ -406,8 +406,9 @@ pub async fn authorize_post(
let user = match sqlx::query!(
r#"
SELECT id, did, email, password_hash, two_factor_enabled,
- preferred_notification_channel as "preferred_notification_channel: NotificationChannel",
- deactivated_at, takedown_ref
+ preferred_comms_channel as "preferred_comms_channel: CommsChannel",
+ deactivated_at, takedown_ref,
+ email_verified, discord_verified, telegram_verified, signal_verified
FROM users
WHERE handle = $1 OR email = $1
"#,
@@ -429,6 +430,13 @@ pub async fn authorize_post(
if user.takedown_ref.is_some() {
return show_login_error("This account has been taken down.", json_response);
}
+ let is_verified = user.email_verified
+ || user.discord_verified
+ || user.telegram_verified
+ || user.signal_verified;
+ if !is_verified {
+ return show_login_error("Please verify your account before logging in.", json_response);
+ }
let password_valid = match bcrypt::verify(&form.password, &user.password_hash) {
Ok(valid) => valid,
Err(_) => return show_login_error("An error occurred. Please try again.", json_response),
@@ -451,7 +459,7 @@ pub async fn authorize_post(
"Failed to enqueue 2FA notification"
);
}
- let channel_name = channel_display_name(user.preferred_notification_channel);
+ let channel_name = channel_display_name(user.preferred_comms_channel);
let redirect_url = format!(
"/oauth/authorize/2fa?request_uri={}&channel={}",
url_encode(&form.request_uri),
@@ -577,7 +585,8 @@ pub async fn authorize_select(
let user = match sqlx::query!(
r#"
SELECT id, two_factor_enabled,
- preferred_notification_channel as "preferred_notification_channel: NotificationChannel"
+ preferred_comms_channel as "preferred_comms_channel: CommsChannel",
+ email_verified, discord_verified, telegram_verified, signal_verified
FROM users
WHERE did = $1
"#,
@@ -600,6 +609,17 @@ pub async fn authorize_select(
)).into_response();
}
};
+ let is_verified = user.email_verified
+ || user.discord_verified
+ || user.telegram_verified
+ || user.signal_verified;
+ if !is_verified {
+ return Html(templates::error_page(
+ "access_denied",
+ Some("Please verify your account before logging in."),
+ ))
+ .into_response();
+ }
if user.two_factor_enabled {
let _ = db::delete_2fa_challenge_by_request_uri(&state.db, &form.request_uri).await;
match db::create_2fa_challenge(&state.db, &form.did, &form.request_uri).await {
@@ -615,7 +635,7 @@ pub async fn authorize_select(
"Failed to enqueue 2FA notification"
);
}
- let channel_name = channel_display_name(user.preferred_notification_channel);
+ let channel_name = channel_display_name(user.preferred_comms_channel);
let redirect_url = format!(
"/oauth/authorize/2fa?request_uri={}&channel={}",
url_encode(&form.request_uri),
@@ -836,7 +856,7 @@ pub async fn authorize_2fa_post(
if !code_valid {
let _ = db::increment_2fa_attempts(&state.db, challenge.id).await;
let channel = match sqlx::query_scalar!(
- r#"SELECT preferred_notification_channel as "channel: NotificationChannel" FROM users WHERE did = $1"#,
+ r#"SELECT preferred_comms_channel as "channel: CommsChannel" FROM users WHERE did = $1"#,
challenge.did
)
.fetch_optional(&state.db)
diff --git a/src/oauth/templates.rs b/src/oauth/templates.rs
index 5c2cfaa..d3ecb61 100644
--- a/src/oauth/templates.rs
+++ b/src/oauth/templates.rs
@@ -369,7 +369,7 @@ pub fn login_page(
Sign In
- Cancel
+ Cancel
diff --git a/tests/account_notifications.rs b/tests/account_notifications.rs
index 279760e..d8c2a72 100644
--- a/tests/account_notifications.rs
+++ b/tests/account_notifications.rs
@@ -1,6 +1,6 @@
mod common;
use common::{base_url, client, create_account_and_login, get_db_connection_string};
-use bspds::notifications::{NewNotification, NotificationType, enqueue_notification};
+use bspds::comms::{NewComms, CommsType, enqueue_comms};
use serde_json::{Value, json};
use sqlx::PgPool;
@@ -26,14 +26,14 @@ async fn test_get_notification_history() {
.expect("User not found");
for i in 0..3 {
- let notification = NewNotification::email(
+ let comms = NewComms::email(
user_id,
- NotificationType::Welcome,
+ CommsType::Welcome,
"test@example.com".to_string(),
format!("Subject {}", i),
format!("Body {}", i),
);
- enqueue_notification(&pool, notification).await.expect("Failed to enqueue");
+ enqueue_comms(&pool, comms).await.expect("Failed to enqueue");
}
let resp = client
diff --git a/tests/admin_email.rs b/tests/admin_email.rs
index dfa9e05..5dd2809 100644
--- a/tests/admin_email.rs
+++ b/tests/admin_email.rs
@@ -39,7 +39,7 @@ async fn test_send_email_success() {
.await
.expect("User not found");
let notification = sqlx::query!(
- "SELECT subject, body, notification_type as \"notification_type: String\" FROM notification_queue WHERE user_id = $1 AND notification_type = 'admin_email' ORDER BY created_at DESC LIMIT 1",
+ "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",
user.id
)
.fetch_one(&pool)
@@ -78,7 +78,7 @@ async fn test_send_email_default_subject() {
.await
.expect("User not found");
let notification = sqlx::query!(
- "SELECT subject FROM notification_queue WHERE user_id = $1 AND notification_type = 'admin_email' AND body = 'Email without subject' LIMIT 1",
+ "SELECT subject FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' AND body = 'Email without subject' LIMIT 1",
user.id
)
.fetch_one(&pool)
diff --git a/tests/notifications.rs b/tests/notifications.rs
index 5ebec14..74bac10 100644
--- a/tests/notifications.rs
+++ b/tests/notifications.rs
@@ -1,7 +1,6 @@
mod common;
-use bspds::notifications::{
- NewNotification, NotificationChannel, NotificationStatus, NotificationType,
- enqueue_notification, enqueue_welcome,
+use bspds::comms::{
+ CommsChannel, CommsStatus, CommsType, NewComms, enqueue_comms, enqueue_welcome,
};
use sqlx::PgPool;
@@ -15,45 +14,45 @@ async fn get_pool() -> PgPool {
}
#[tokio::test]
-async fn test_enqueue_notification() {
+async fn test_enqueue_comms() {
let pool = get_pool().await;
let (_, did) = common::create_account_and_login(&common::client()).await;
let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
.fetch_one(&pool)
.await
.expect("User not found");
- let notification = NewNotification::email(
+ let item = NewComms::email(
user_id,
- NotificationType::Welcome,
+ CommsType::Welcome,
"test@example.com".to_string(),
"Test Subject".to_string(),
"Test body".to_string(),
);
- let notification_id = enqueue_notification(&pool, notification)
+ let comms_id = enqueue_comms(&pool, item)
.await
- .expect("Failed to enqueue notification");
+ .expect("Failed to enqueue comms");
let row = sqlx::query!(
r#"
SELECT
id, user_id, recipient, subject, body,
- channel as "channel: NotificationChannel",
- notification_type as "notification_type: NotificationType",
- status as "status: NotificationStatus"
- FROM notification_queue
+ channel as "channel: CommsChannel",
+ comms_type as "comms_type: CommsType",
+ status as "status: CommsStatus"
+ FROM comms_queue
WHERE id = $1
"#,
- notification_id
+ comms_id
)
.fetch_one(&pool)
.await
- .expect("Notification not found");
+ .expect("Comms not found");
assert_eq!(row.user_id, user_id);
assert_eq!(row.recipient, "test@example.com");
assert_eq!(row.subject.as_deref(), Some("Test Subject"));
assert_eq!(row.body, "Test body");
- assert_eq!(row.channel, NotificationChannel::Email);
- assert_eq!(row.notification_type, NotificationType::Welcome);
- assert_eq!(row.status, NotificationStatus::Pending);
+ assert_eq!(row.channel, CommsChannel::Email);
+ assert_eq!(row.comms_type, CommsType::Welcome);
+ assert_eq!(row.status, CommsStatus::Pending);
}
#[tokio::test]
@@ -64,30 +63,30 @@ async fn test_enqueue_welcome() {
.fetch_one(&pool)
.await
.expect("User not found");
- let notification_id = enqueue_welcome(&pool, user_row.id, "example.com")
+ let comms_id = enqueue_welcome(&pool, user_row.id, "example.com")
.await
- .expect("Failed to enqueue welcome notification");
+ .expect("Failed to enqueue welcome comms");
let row = sqlx::query!(
r#"
SELECT
recipient, subject, body,
- notification_type as "notification_type: NotificationType"
- FROM notification_queue
+ comms_type as "comms_type: CommsType"
+ FROM comms_queue
WHERE id = $1
"#,
- notification_id
+ comms_id
)
.fetch_one(&pool)
.await
- .expect("Notification not found");
+ .expect("Comms not found");
assert_eq!(Some(row.recipient), user_row.email);
assert_eq!(row.subject.as_deref(), Some("Welcome to example.com"));
assert!(row.body.contains(&format!("@{}", user_row.handle)));
- assert_eq!(row.notification_type, NotificationType::Welcome);
+ assert_eq!(row.comms_type, CommsType::Welcome);
}
#[tokio::test]
-async fn test_notification_queue_status_index() {
+async fn test_comms_queue_status_index() {
let pool = get_pool().await;
let (_, did) = common::create_account_and_login(&common::client()).await;
let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
@@ -95,7 +94,7 @@ async fn test_notification_queue_status_index() {
.await
.expect("User not found");
let initial_count: i64 = sqlx::query_scalar!(
- "SELECT COUNT(*) FROM notification_queue WHERE status = 'pending' AND user_id = $1",
+ "SELECT COUNT(*) FROM comms_queue WHERE status = 'pending' AND user_id = $1",
user_id
)
.fetch_one(&pool)
@@ -103,19 +102,19 @@ async fn test_notification_queue_status_index() {
.expect("Failed to count")
.unwrap_or(0);
for i in 0..5 {
- let notification = NewNotification::email(
+ let item = NewComms::email(
user_id,
- NotificationType::PasswordReset,
+ CommsType::PasswordReset,
format!("test{}@example.com", i),
"Test".to_string(),
"Body".to_string(),
);
- enqueue_notification(&pool, notification)
+ enqueue_comms(&pool, item)
.await
.expect("Failed to enqueue");
}
let final_count: i64 = sqlx::query_scalar!(
- "SELECT COUNT(*) FROM notification_queue WHERE status = 'pending' AND user_id = $1",
+ "SELECT COUNT(*) FROM comms_queue WHERE status = 'pending' AND user_id = $1",
user_id
)
.fetch_one(&pool)
diff --git a/tests/oauth.rs b/tests/oauth.rs
index 774c3b4..b544530 100644
--- a/tests/oauth.rs
+++ b/tests/oauth.rs
@@ -2,7 +2,8 @@ mod common;
mod helpers;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::Utc;
-use common::{base_url, client, create_account_and_login, get_db_connection_string};
+use common::{base_url, client, get_db_connection_string};
+use helpers::verify_new_account;
use reqwest::{StatusCode, redirect};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
@@ -124,6 +125,7 @@ async fn test_full_oauth_flow() {
assert_eq!(create_res.status(), StatusCode::OK);
let account: Value = create_res.json().await.unwrap();
let user_did = account["did"].as_str().unwrap();
+ verify_new_account(&http_client, user_did).await;
let redirect_uri = "https://example.com/oauth/callback";
let mock_client = setup_mock_client_metadata(redirect_uri).await;
let client_id = mock_client.uri();
@@ -261,6 +263,7 @@ async fn test_oauth_2fa_flow() {
assert_eq!(create_res.status(), StatusCode::OK);
let account: Value = create_res.json().await.unwrap();
let user_did = account["did"].as_str().unwrap();
+ verify_new_account(&http_client, user_did).await;
let db_url = get_db_connection_string().await;
let pool = sqlx::postgres::PgPoolOptions::new().max_connections(1).connect(&db_url).await.unwrap();
sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
@@ -324,6 +327,7 @@ async fn test_oauth_2fa_lockout() {
.send().await.unwrap();
let account: Value = create_res.json().await.unwrap();
let user_did = account["did"].as_str().unwrap();
+ verify_new_account(&http_client, user_did).await;
let db_url = get_db_connection_string().await;
let pool = sqlx::postgres::PgPoolOptions::new().max_connections(1).connect(&db_url).await.unwrap();
sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
@@ -375,6 +379,7 @@ async fn test_account_selector_with_2fa() {
.send().await.unwrap();
let account: Value = create_res.json().await.unwrap();
let user_did = account["did"].as_str().unwrap().to_string();
+ verify_new_account(&http_client, &user_did).await;
let redirect_uri = "https://example.com/selector-2fa-callback";
let mock_client = setup_mock_client_metadata(redirect_uri).await;
let client_id = mock_client.uri();
@@ -451,10 +456,12 @@ async fn test_oauth_state_encoding() {
let handle = format!("state-special-{}", ts);
let email = format!("state-special-{}@example.com", ts);
let password = "state-special-password";
- http_client
+ let create_res = http_client
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
.json(&json!({ "handle": handle, "email": email, "password": password }))
.send().await.unwrap();
+ let account: Value = create_res.json().await.unwrap();
+ verify_new_account(&http_client, account["did"].as_str().unwrap()).await;
let redirect_uri = "https://example.com/state-special-callback";
let mock_client = setup_mock_client_metadata(redirect_uri).await;
let client_id = mock_client.uri();
diff --git a/tests/oauth_security.rs b/tests/oauth_security.rs
index a83f872..94d115a 100644
--- a/tests/oauth_security.rs
+++ b/tests/oauth_security.rs
@@ -45,9 +45,12 @@ async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer {
async fn get_oauth_tokens(http_client: &reqwest::Client, url: &str) -> (String, String, String) {
let ts = Utc::now().timestamp_millis();
let handle = format!("sec-test-{}", ts);
- http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+ let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
.json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "security-test-password" }))
.send().await.unwrap();
+ let account: Value = create_res.json().await.unwrap();
+ let did = account["did"].as_str().unwrap();
+ verify_new_account(http_client, did).await;
let redirect_uri = "https://example.com/sec-callback";
let mock_client = setup_mock_client_metadata(redirect_uri).await;
let client_id = mock_client.uri();
@@ -129,9 +132,11 @@ async fn test_pkce_security() {
assert_eq!(res.status(), StatusCode::BAD_REQUEST, "Missing PKCE challenge should be rejected");
let ts = Utc::now().timestamp_millis();
let handle = format!("pkce-attack-{}", ts);
- http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+ let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
.json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "pkce-password" }))
.send().await.unwrap();
+ let account: Value = create_res.json().await.unwrap();
+ verify_new_account(&http_client, account["did"].as_str().unwrap()).await;
let (_, code_challenge) = generate_pkce();
let (attacker_verifier, _) = generate_pkce();
let par_body: Value = http_client.post(format!("{}/oauth/par", url))
@@ -158,9 +163,11 @@ async fn test_replay_attacks() {
let http_client = client();
let ts = Utc::now().timestamp_millis();
let handle = format!("replay-{}", ts);
- http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+ let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
.json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "replay-password" }))
.send().await.unwrap();
+ let account: Value = create_res.json().await.unwrap();
+ verify_new_account(&http_client, account["did"].as_str().unwrap()).await;
let redirect_uri = "https://example.com/replay-callback";
let mock_client = setup_mock_client_metadata(redirect_uri).await;
let client_id = mock_client.uri();
@@ -243,9 +250,11 @@ async fn test_oauth_security_boundaries() {
let client_id_b = mock_b.uri();
let ts2 = Utc::now().timestamp_millis();
let handle2 = format!("cross-{}", ts2);
- http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+ let create_res2 = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
.json(&json!({ "handle": handle2, "email": format!("{}@example.com", handle2), "password": "cross-password" }))
.send().await.unwrap();
+ let account2: Value = create_res2.json().await.unwrap();
+ verify_new_account(&http_client, account2["did"].as_str().unwrap()).await;
let (code_verifier2, code_challenge2) = generate_pkce();
let par_a: Value = http_client.post(format!("{}/oauth/par", url))
.form(&[("response_type", "code"), ("client_id", &client_id_a), ("redirect_uri", redirect_uri_a),
diff --git a/tests/password_reset.rs b/tests/password_reset.rs
index 8fcd139..84f814d 100644
--- a/tests/password_reset.rs
+++ b/tests/password_reset.rs
@@ -373,7 +373,7 @@ async fn test_reset_password_creates_notification() {
.await
.expect("User not found");
let initial_count: i64 = sqlx::query_scalar!(
- "SELECT COUNT(*) FROM notification_queue WHERE user_id = $1 AND notification_type = 'password_reset'",
+ "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'",
user.id
)
.fetch_one(&pool)
@@ -391,7 +391,7 @@ async fn test_reset_password_creates_notification() {
.expect("Failed to request password reset");
assert_eq!(res.status(), StatusCode::OK);
let final_count: i64 = sqlx::query_scalar!(
- "SELECT COUNT(*) FROM notification_queue WHERE user_id = $1 AND notification_type = 'password_reset'",
+ "SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'",
user.id
)
.fetch_one(&pool)
diff --git a/tests/security_fixes.rs b/tests/security_fixes.rs
index 7285081..315d100 100644
--- a/tests/security_fixes.rs
+++ b/tests/security_fixes.rs
@@ -1,6 +1,6 @@
mod common;
use bspds::image::{ImageError, ImageProcessor};
-use bspds::notifications::{SendError, is_valid_phone_number, sanitize_header_value};
+use bspds::comms::{SendError, is_valid_phone_number, sanitize_header_value};
use bspds::oauth::templates::{error_page, login_page, success_page};
#[test]