···1+ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'channel_verification';
2+3+CREATE TABLE IF NOT EXISTS channel_verifications (
4+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
5+ channel notification_channel NOT NULL,
6+ code TEXT NOT NULL,
7+ expires_at TIMESTAMPTZ NOT NULL,
8+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
9+ PRIMARY KEY (user_id, channel)
10+);
11+12+CREATE INDEX IF NOT EXISTS idx_channel_verifications_expires ON channel_verifications(expires_at);
···1+ALTER TABLE channel_verifications ADD COLUMN pending_identifier TEXT;
2+3+INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at)
4+SELECT id, 'email', email_confirmation_code, email_pending_verification, email_confirmation_code_expires_at
5+FROM users
6+WHERE email_confirmation_code IS NOT NULL AND email_confirmation_code_expires_at IS NOT NULL;
7+8+ALTER TABLE users
9+DROP COLUMN email_confirmation_code,
10+DROP COLUMN email_confirmation_code_expires_at,
11+DROP COLUMN email_pending_verification;
+2
src/api/admin/mod.rs
···1pub mod account;
2pub mod invite;
03pub mod status;
45pub use account::{
···9pub use invite::{
10 disable_account_invites, disable_invite_codes, enable_account_invites, get_invite_codes,
11};
012pub use status::{get_subject_status, update_subject_status};
···1pub mod account;
2pub mod invite;
3+pub mod server_stats;
4pub mod status;
56pub use account::{
···10pub use invite::{
11 disable_account_invites, disable_invite_codes, enable_account_invites, get_invite_codes,
12};
13+pub use server_stats::get_server_stats;
14pub use status::{get_subject_status, update_subject_status};
···13pub mod server;
14pub mod temp;
15pub mod validation;
01617pub use error::ApiError;
18pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_did, validate_limit};
···13pub mod server;
14pub mod temp;
15pub mod validation;
16+pub mod verification;
1718pub use error::ApiError;
19pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_did, validate_limit};
···1+mod common;
2+use common::{base_url, client, create_account_and_login, get_db_connection_string};
3+use bspds::notifications::{NewNotification, NotificationType, enqueue_notification};
4+use serde_json::{Value, json};
5+use sqlx::PgPool;
6+7+async fn get_pool() -> PgPool {
8+ let conn_str = get_db_connection_string().await;
9+ sqlx::postgres::PgPoolOptions::new()
10+ .max_connections(5)
11+ .connect(&conn_str)
12+ .await
13+ .expect("Failed to connect to test database")
14+}
15+16+#[tokio::test]
17+async fn test_get_notification_history() {
18+ let client = client();
19+ let base = base_url().await;
20+ let pool = get_pool().await;
21+ let (token, did) = create_account_and_login(&client).await;
22+23+ let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
24+ .fetch_one(&pool)
25+ .await
26+ .expect("User not found");
27+28+ for i in 0..3 {
29+ let notification = NewNotification::email(
30+ user_id,
31+ NotificationType::Welcome,
32+ "test@example.com".to_string(),
33+ format!("Subject {}", i),
34+ format!("Body {}", i),
35+ );
36+ enqueue_notification(&pool, notification).await.expect("Failed to enqueue");
37+ }
38+39+ let resp = client
40+ .get(format!("{}/xrpc/com.bspds.account.getNotificationHistory", base))
41+ .header("Authorization", format!("Bearer {}", token))
42+ .send()
43+ .await
44+ .unwrap();
45+46+ assert_eq!(resp.status(), 200);
47+ let body: Value = resp.json().await.unwrap();
48+ let notifications = body["notifications"].as_array().unwrap();
49+ assert_eq!(notifications.len(), 5);
50+51+ assert_eq!(notifications[0]["subject"], "Subject 2");
52+ assert_eq!(notifications[1]["subject"], "Subject 1");
53+ assert_eq!(notifications[2]["subject"], "Subject 0");
54+}
55+56+#[tokio::test]
57+async fn test_verify_channel_discord() {
58+ let client = client();
59+ let base = base_url().await;
60+ let (token, did) = create_account_and_login(&client).await;
61+62+ let prefs = json!({
63+ "discordId": "123456789"
64+ });
65+ let resp = client
66+ .post(format!("{}/xrpc/com.bspds.account.updateNotificationPrefs", base))
67+ .header("Authorization", format!("Bearer {}", token))
68+ .json(&prefs)
69+ .send()
70+ .await
71+ .unwrap();
72+ assert_eq!(resp.status(), 200);
73+ let body: Value = resp.json().await.unwrap();
74+ assert!(body["verificationRequired"].as_array().unwrap().contains(&json!("discord")));
75+76+ let pool = get_pool().await;
77+ let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
78+ .fetch_one(&pool)
79+ .await
80+ .expect("User not found");
81+82+ let code: String = sqlx::query_scalar!(
83+ "SELECT code FROM channel_verifications WHERE user_id = $1 AND channel = 'discord'",
84+ user_id
85+ )
86+ .fetch_one(&pool)
87+ .await
88+ .expect("Verification code not found");
89+90+ let input = json!({
91+ "channel": "discord",
92+ "code": code
93+ });
94+ let resp = client
95+ .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base))
96+ .header("Authorization", format!("Bearer {}", token))
97+ .json(&input)
98+ .send()
99+ .await
100+ .unwrap();
101+ assert_eq!(resp.status(), 200);
102+103+ let resp = client
104+ .get(format!("{}/xrpc/com.bspds.account.getNotificationPrefs", base))
105+ .header("Authorization", format!("Bearer {}", token))
106+ .send()
107+ .await
108+ .unwrap();
109+ let body: Value = resp.json().await.unwrap();
110+ assert_eq!(body["discordVerified"], true);
111+ assert_eq!(body["discordId"], "123456789");
112+}
113+114+#[tokio::test]
115+async fn test_verify_channel_invalid_code() {
116+ let client = client();
117+ let base = base_url().await;
118+ let (token, _did) = create_account_and_login(&client).await;
119+120+ let prefs = json!({
121+ "telegramUsername": "testuser"
122+ });
123+ let resp = client
124+ .post(format!("{}/xrpc/com.bspds.account.updateNotificationPrefs", base))
125+ .header("Authorization", format!("Bearer {}", token))
126+ .json(&prefs)
127+ .send()
128+ .await
129+ .unwrap();
130+ assert_eq!(resp.status(), 200);
131+132+ let input = json!({
133+ "channel": "telegram",
134+ "code": "000000"
135+ });
136+ let resp = client
137+ .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base))
138+ .header("Authorization", format!("Bearer {}", token))
139+ .json(&input)
140+ .send()
141+ .await
142+ .unwrap();
143+ assert_eq!(resp.status(), 400);
144+}
145+146+#[tokio::test]
147+async fn test_verify_channel_not_set() {
148+ let client = client();
149+ let base = base_url().await;
150+ let (token, _did) = create_account_and_login(&client).await;
151+152+ let input = json!({
153+ "channel": "signal",
154+ "code": "123456"
155+ });
156+ let resp = client
157+ .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base))
158+ .header("Authorization", format!("Bearer {}", token))
159+ .json(&input)
160+ .send()
161+ .await
162+ .unwrap();
163+ assert_eq!(resp.status(), 400);
164+}
165+166+#[tokio::test]
167+async fn test_update_email_via_notification_prefs() {
168+ let client = client();
169+ let base = base_url().await;
170+ let pool = get_pool().await;
171+ let (token, did) = create_account_and_login(&client).await;
172+173+ let prefs = json!({
174+ "email": "newemail@example.com"
175+ });
176+ let resp = client
177+ .post(format!("{}/xrpc/com.bspds.account.updateNotificationPrefs", base))
178+ .header("Authorization", format!("Bearer {}", token))
179+ .json(&prefs)
180+ .send()
181+ .await
182+ .unwrap();
183+ assert_eq!(resp.status(), 200);
184+ let body: Value = resp.json().await.unwrap();
185+ assert!(body["verificationRequired"].as_array().unwrap().contains(&json!("email")));
186+187+ let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
188+ .fetch_one(&pool)
189+ .await
190+ .expect("User not found");
191+192+ let code: String = sqlx::query_scalar!(
193+ "SELECT code FROM channel_verifications WHERE user_id = $1 AND channel = 'email'",
194+ user_id
195+ )
196+ .fetch_one(&pool)
197+ .await
198+ .expect("Verification code not found");
199+200+ let input = json!({
201+ "channel": "email",
202+ "code": code
203+ });
204+ let resp = client
205+ .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base))
206+ .header("Authorization", format!("Bearer {}", token))
207+ .json(&input)
208+ .send()
209+ .await
210+ .unwrap();
211+ assert_eq!(resp.status(), 200);
212+213+ let resp = client
214+ .get(format!("{}/xrpc/com.bspds.account.getNotificationPrefs", base))
215+ .header("Authorization", format!("Bearer {}", token))
216+ .send()
217+ .await
218+ .unwrap();
219+ let body: Value = resp.json().await.unwrap();
220+ assert_eq!(body["email"], "newemail@example.com");
221+}
+41
tests/admin_stats.rs
···00000000000000000000000000000000000000000
···1+mod common;
2+use common::{base_url, client, create_account_and_login};
3+use serde_json::Value;
4+5+#[tokio::test]
6+async fn test_get_server_stats() {
7+ let client = client();
8+ let base = base_url().await;
9+ let (token1, _) = create_account_and_login(&client).await;
10+11+ let (_, _) = create_account_and_login(&client).await;
12+13+ let resp = client
14+ .get(format!("{}/xrpc/com.bspds.admin.getServerStats", base))
15+ .header("Authorization", format!("Bearer {}", token1))
16+ .send()
17+ .await
18+ .unwrap();
19+20+ assert_eq!(resp.status(), 200);
21+ let body: Value = resp.json().await.unwrap();
22+23+ let user_count = body["userCount"].as_i64().unwrap();
24+ assert!(user_count >= 2);
25+26+ assert!(body["repoCount"].is_number());
27+ assert!(body["recordCount"].is_number());
28+ assert!(body["blobStorageBytes"].is_number());
29+}
30+31+#[tokio::test]
32+async fn test_get_server_stats_no_auth() {
33+ let client = client();
34+ let base = base_url().await;
35+ let resp = client
36+ .get(format!("{}/xrpc/com.bspds.admin.getServerStats", base))
37+ .send()
38+ .await
39+ .unwrap();
40+ assert_eq!(resp.status(), 401);
41+}
+9-6
tests/common/mod.rs
···79 SERVER_URL.get_or_init(|| {
80 let (tx, rx) = std::sync::mpsc::channel();
81 std::thread::spawn(move || {
00082 if std::env::var("DOCKER_HOST").is_err() {
83 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
84 let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
···406 .await
407 .expect("Failed to connect to test database");
408 let verification_code: String = sqlx::query_scalar!(
409- "SELECT email_confirmation_code FROM users WHERE did = $1",
410 did
411 )
412 .fetch_one(&pool)
413 .await
414- .expect("Failed to get verification code")
415- .expect("No verification code found");
416 let confirm_payload = json!({
417 "did": did,
418 "verificationCode": verification_code
···548 .await
549 .expect("Failed to connect to test database");
550 let verification_code: String = sqlx::query_scalar!(
551- "SELECT email_confirmation_code FROM users WHERE did = $1",
552 &did
553 )
554 .fetch_one(&pool)
555 .await
556- .expect("Failed to get verification code")
557- .expect("No verification code found");
558 let confirm_payload = json!({
559 "did": did,
560 "verificationCode": verification_code
···79 SERVER_URL.get_or_init(|| {
80 let (tx, rx) = std::sync::mpsc::channel();
81 std::thread::spawn(move || {
82+ unsafe {
83+ std::env::set_var("BSPDS_ALLOW_INSECURE_SECRETS", "1");
84+ }
85 if std::env::var("DOCKER_HOST").is_err() {
86 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
87 let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
···409 .await
410 .expect("Failed to connect to test database");
411 let verification_code: String = sqlx::query_scalar!(
412+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
413 did
414 )
415 .fetch_one(&pool)
416 .await
417+ .expect("Failed to get verification code");
418+419 let confirm_payload = json!({
420 "did": did,
421 "verificationCode": verification_code
···551 .await
552 .expect("Failed to connect to test database");
553 let verification_code: String = sqlx::query_scalar!(
554+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
555 &did
556 )
557 .fetch_one(&pool)
558 .await
559+ .expect("Failed to get verification code");
560+561 let confirm_payload = json!({
562 "did": did,
563 "verificationCode": verification_code
+34-19
tests/email_update.rs
···59 assert_eq!(res.status(), StatusCode::OK);
60 let body: Value = res.json().await.expect("Invalid JSON");
61 assert_eq!(body["tokenRequired"], true);
62- let user = sqlx::query!(
63- "SELECT email_pending_verification, email_confirmation_code, email FROM users WHERE handle = $1",
064 handle
65 )
66 .fetch_one(&pool)
67 .await
68- .expect("User not found");
069 assert_eq!(
70- user.email_pending_verification.as_deref(),
71 Some(new_email.as_str())
72 );
73- assert!(user.email_confirmation_code.is_some());
74- let code = user.email_confirmation_code.unwrap();
75 let res = client
76 .post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
77 .bearer_auth(&access_jwt)
···84 .expect("Failed to confirm email");
85 assert_eq!(res.status(), StatusCode::OK);
86 let user = sqlx::query!(
87- "SELECT email, email_pending_verification, email_confirmation_code FROM users WHERE handle = $1",
88 handle
89 )
90 .fetch_one(&pool)
91 .await
92 .expect("User not found");
93 assert_eq!(user.email, Some(new_email));
94- assert!(user.email_pending_verification.is_none());
95- assert!(user.email_confirmation_code.is_none());
000000096}
9798#[tokio::test]
···174 .await
175 .expect("Failed to request email update");
176 assert_eq!(res.status(), StatusCode::OK);
177- let user = sqlx::query!(
178- "SELECT email_confirmation_code FROM users WHERE handle = $1",
179 handle
180 )
181 .fetch_one(&pool)
182 .await
183- .expect("User not found");
184- let code = user.email_confirmation_code.unwrap();
185 let res = client
186 .post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
187 .bearer_auth(&access_jwt)
···293 .await
294 .expect("Failed to request email update");
295 assert_eq!(res.status(), StatusCode::OK);
296- let user = sqlx::query!(
297- "SELECT email_confirmation_code FROM users WHERE handle = $1",
298 handle
299 )
300 .fetch_one(&pool)
301 .await
302- .expect("User not found");
303- let code = user.email_confirmation_code.unwrap();
304 let res = client
305 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
306 .bearer_auth(&access_jwt)
···313 .expect("Failed to update email");
314 assert_eq!(res.status(), StatusCode::OK);
315 let user = sqlx::query!(
316- "SELECT email, email_pending_verification FROM users WHERE handle = $1",
317 handle
318 )
319 .fetch_one(&pool)
320 .await
321 .expect("User not found");
322 assert_eq!(user.email, Some(new_email));
323- assert!(user.email_pending_verification.is_none());
0000000324}
325326#[tokio::test]
···59 assert_eq!(res.status(), StatusCode::OK);
60 let body: Value = res.json().await.expect("Invalid JSON");
61 assert_eq!(body["tokenRequired"], true);
62+63+ let verification = sqlx::query!(
64+ "SELECT pending_identifier, code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
65 handle
66 )
67 .fetch_one(&pool)
68 .await
69+ .expect("Verification not found");
70+71 assert_eq!(
72+ verification.pending_identifier.as_deref(),
73 Some(new_email.as_str())
74 );
75+ let code = verification.code;
076 let res = client
77 .post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
78 .bearer_auth(&access_jwt)
···85 .expect("Failed to confirm email");
86 assert_eq!(res.status(), StatusCode::OK);
87 let user = sqlx::query!(
88+ "SELECT email FROM users WHERE handle = $1",
89 handle
90 )
91 .fetch_one(&pool)
92 .await
93 .expect("User not found");
94 assert_eq!(user.email, Some(new_email));
95+96+ let verification = sqlx::query!(
97+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
98+ handle
99+ )
100+ .fetch_optional(&pool)
101+ .await
102+ .expect("DB error");
103+ assert!(verification.is_none());
104}
105106#[tokio::test]
···182 .await
183 .expect("Failed to request email update");
184 assert_eq!(res.status(), StatusCode::OK);
185+ let verification = sqlx::query!(
186+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
187 handle
188 )
189 .fetch_one(&pool)
190 .await
191+ .expect("Verification not found");
192+ let code = verification.code;
193 let res = client
194 .post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
195 .bearer_auth(&access_jwt)
···301 .await
302 .expect("Failed to request email update");
303 assert_eq!(res.status(), StatusCode::OK);
304+ let verification = sqlx::query!(
305+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
306 handle
307 )
308 .fetch_one(&pool)
309 .await
310+ .expect("Verification not found");
311+ let code = verification.code;
312 let res = client
313 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
314 .bearer_auth(&access_jwt)
···321 .expect("Failed to update email");
322 assert_eq!(res.status(), StatusCode::OK);
323 let user = sqlx::query!(
324+ "SELECT email FROM users WHERE handle = $1",
325 handle
326 )
327 .fetch_one(&pool)
328 .await
329 .expect("User not found");
330 assert_eq!(user.email, Some(new_email));
331+ let verification = sqlx::query!(
332+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
333+ handle
334+ )
335+ .fetch_optional(&pool)
336+ .await
337+ .expect("DB error");
338+ assert!(verification.is_none());
339}
340341#[tokio::test]
+2-3
tests/jwt_security.rs
···872 .await
873 .expect("Failed to connect to test database");
874 let verification_code: String = sqlx::query_scalar!(
875- "SELECT email_confirmation_code FROM users WHERE did = $1",
876 did
877 )
878 .fetch_one(&pool)
879 .await
880- .expect("Failed to get verification code")
881- .expect("No verification code found");
882 let confirm_res = http_client
883 .post(format!("{}/xrpc/com.atproto.server.confirmSignup", url))
884 .json(&json!({
···872 .await
873 .expect("Failed to connect to test database");
874 let verification_code: String = sqlx::query_scalar!(
875+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
876 did
877 )
878 .fetch_one(&pool)
879 .await
880+ .expect("Failed to get verification code");
0881 let confirm_res = http_client
882 .post(format!("{}/xrpc/com.atproto.server.confirmSignup", url))
883 .json(&json!({