···5050 }
5151 }
52525353+ let handleHasDot = $derived(handle.includes('.'))
5454+5355 function validateForm(): string | null {
5456 if (!handle.trim()) return 'Handle is required'
5757+ if (handle.includes('.')) return 'Handle cannot contain dots. You can set up a custom domain handle after creating your account.'
5558 if (!password) return 'Password is required'
5659 if (password.length < 8) return 'Password must be at least 8 characters'
5760 if (password !== confirmPassword) return 'Passwords do not match'
···152155 disabled={submitting}
153156 required
154157 />
155155- {#if fullHandle()}
158158+ {#if handleHasDot}
159159+ <p class="hint warning">Custom domain handles can be set up after account creation in Settings.</p>
160160+ {:else if fullHandle()}
156161 <p class="hint">Your full handle will be: @{fullHandle()}</p>
157162 {/if}
158163 </div>
···389394 font-size: 0.75rem;
390395 color: var(--text-secondary);
391396 margin: 0.25rem 0 0 0;
397397+ }
398398+ .hint.warning {
399399+ color: var(--warning-text, #856404);
392400 }
393401 .verification-section {
394402 border: 1px solid var(--border-color-light);
+90-19
migrations/20251211_initial_schema.sql
···11-CREATE TYPE notification_channel AS ENUM ('email', 'discord', 'telegram', 'signal');
22-CREATE TYPE notification_status AS ENUM ('pending', 'processing', 'sent', 'failed');
33-CREATE TYPE notification_type AS ENUM (
11+CREATE TYPE comms_channel AS ENUM ('email', 'discord', 'telegram', 'signal');
22+CREATE TYPE comms_status AS ENUM ('pending', 'processing', 'sent', 'failed');
33+CREATE TYPE comms_type AS ENUM (
44 'welcome',
55 'email_verification',
66 'password_reset',
···88 'account_deletion',
99 'admin_email',
1010 'plc_operation',
1111- 'two_factor_code'
1111+ 'two_factor_code',
1212+ 'channel_verification'
1213);
1314CREATE TABLE IF NOT EXISTS users (
1415 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
···2122 deactivated_at TIMESTAMPTZ,
2223 invites_disabled BOOLEAN DEFAULT FALSE,
2324 takedown_ref TEXT,
2424- preferred_notification_channel notification_channel NOT NULL DEFAULT 'email',
2525+ preferred_comms_channel comms_channel NOT NULL DEFAULT 'email',
2526 password_reset_code TEXT,
2627 password_reset_code_expires_at TIMESTAMPTZ,
2727- email_pending_verification TEXT,
2828- email_confirmation_code TEXT,
2929- email_confirmation_code_expires_at TIMESTAMPTZ,
3030- email_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
2828+ email_verified BOOLEAN NOT NULL DEFAULT FALSE,
3129 two_factor_enabled BOOLEAN NOT NULL DEFAULT FALSE,
3230 discord_id TEXT,
3331 discord_verified BOOLEAN NOT NULL DEFAULT FALSE,
3432 telegram_username TEXT,
3533 telegram_verified BOOLEAN NOT NULL DEFAULT FALSE,
3634 signal_number TEXT,
3737- signal_verified BOOLEAN NOT NULL DEFAULT FALSE
3535+ signal_verified BOOLEAN NOT NULL DEFAULT FALSE,
3636+ is_admin BOOLEAN NOT NULL DEFAULT FALSE,
3737+ migrated_to_pds TEXT,
3838+ migrated_at TIMESTAMPTZ
3839);
3940CREATE INDEX IF NOT EXISTS idx_users_password_reset_code ON users(password_reset_code) WHERE password_reset_code IS NOT NULL;
4040-CREATE INDEX IF NOT EXISTS idx_users_email_confirmation_code ON users(email_confirmation_code) WHERE email_confirmation_code IS NOT NULL;
4141CREATE INDEX IF NOT EXISTS idx_users_discord_id ON users(discord_id) WHERE discord_id IS NOT NULL;
4242CREATE INDEX IF NOT EXISTS idx_users_telegram_username ON users(telegram_username) WHERE telegram_username IS NOT NULL;
4343CREATE INDEX IF NOT EXISTS idx_users_signal_number ON users(signal_number) WHERE signal_number IS NOT NULL;
4444+CREATE INDEX IF NOT EXISTS idx_users_email ON users(email) WHERE email IS NOT NULL;
4445CREATE TABLE IF NOT EXISTS invite_codes (
4546 code TEXT PRIMARY KEY,
4647 available_uses INT NOT NULL DEFAULT 1,
···4849 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4950 disabled BOOLEAN DEFAULT FALSE
5051);
5252+CREATE INDEX IF NOT EXISTS idx_invite_codes_created_by ON invite_codes(created_by_user);
5153CREATE TABLE IF NOT EXISTS invite_code_uses (
5254 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
5355 code TEXT NOT NULL REFERENCES invite_codes(code),
···8688 UNIQUE(repo_id, collection, rkey)
8789);
8890CREATE INDEX idx_records_repo_rev ON records(repo_rev);
9191+CREATE INDEX IF NOT EXISTS idx_records_repo_collection ON records(repo_id, collection);
9292+CREATE INDEX IF NOT EXISTS idx_records_repo_collection_created ON records(repo_id, collection, created_at DESC);
8993CREATE TABLE IF NOT EXISTS blobs (
9094 cid TEXT PRIMARY KEY,
9195 mime_type TEXT NOT NULL,
···9599 takedown_ref TEXT,
96100 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
97101);
102102+CREATE INDEX IF NOT EXISTS idx_blobs_created_by_user ON blobs(created_by_user, created_at DESC);
98103CREATE TABLE IF NOT EXISTS app_passwords (
99104 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
100105 user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
···104109 privileged BOOLEAN NOT NULL DEFAULT FALSE,
105110 UNIQUE(user_id, name)
106111);
112112+CREATE INDEX IF NOT EXISTS idx_app_passwords_user_id ON app_passwords(user_id);
107113CREATE TABLE reports (
108114 id BIGINT PRIMARY KEY,
109115 reason_type TEXT NOT NULL,
···118124 expires_at TIMESTAMPTZ NOT NULL,
119125 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
120126);
121121-CREATE TABLE IF NOT EXISTS notification_queue (
127127+CREATE TABLE IF NOT EXISTS comms_queue (
122128 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
123129 user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
124124- channel notification_channel NOT NULL DEFAULT 'email',
125125- notification_type notification_type NOT NULL,
126126- status notification_status NOT NULL DEFAULT 'pending',
130130+ channel comms_channel NOT NULL DEFAULT 'email',
131131+ comms_type comms_type NOT NULL,
132132+ status comms_status NOT NULL DEFAULT 'pending',
127133 recipient TEXT NOT NULL,
128134 subject TEXT,
129135 body TEXT NOT NULL,
···136142 scheduled_for TIMESTAMPTZ NOT NULL DEFAULT NOW(),
137143 processed_at TIMESTAMPTZ
138144);
139139-CREATE INDEX idx_notification_queue_status_scheduled
140140- ON notification_queue(status, scheduled_for)
145145+CREATE INDEX idx_comms_queue_status_scheduled
146146+ ON comms_queue(status, scheduled_for)
141147 WHERE status = 'pending';
142142-CREATE INDEX idx_notification_queue_user_id ON notification_queue(user_id);
148148+CREATE INDEX idx_comms_queue_user_id ON comms_queue(user_id);
143149CREATE TABLE IF NOT EXISTS reserved_signing_keys (
144150 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
145151 did TEXT,
···160166 prev_cid TEXT,
161167 ops JSONB,
162168 blobs TEXT[],
163163- blocks_cids TEXT[]
169169+ blocks_cids TEXT[],
170170+ prev_data_cid TEXT,
171171+ handle TEXT,
172172+ active BOOLEAN,
173173+ status TEXT
164174);
165175CREATE INDEX idx_repo_seq_seq ON repo_seq(seq);
166176CREATE INDEX idx_repo_seq_did ON repo_seq(did);
177177+CREATE INDEX IF NOT EXISTS idx_repo_seq_did_seq ON repo_seq(did, seq DESC);
167178CREATE TABLE IF NOT EXISTS session_tokens (
168179 id SERIAL PRIMARY KEY,
169180 did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
···275286);
276287CREATE INDEX idx_oauth_2fa_challenge_request_uri ON oauth_2fa_challenge(request_uri);
277288CREATE INDEX idx_oauth_2fa_challenge_expires ON oauth_2fa_challenge(expires_at);
289289+CREATE TABLE IF NOT EXISTS channel_verifications (
290290+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
291291+ channel comms_channel NOT NULL,
292292+ code TEXT NOT NULL,
293293+ pending_identifier TEXT,
294294+ expires_at TIMESTAMPTZ NOT NULL,
295295+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
296296+ PRIMARY KEY (user_id, channel)
297297+);
298298+CREATE INDEX IF NOT EXISTS idx_channel_verifications_expires ON channel_verifications(expires_at);
299299+CREATE TABLE oauth_scope_preference (
300300+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
301301+ did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
302302+ client_id TEXT NOT NULL,
303303+ scope TEXT NOT NULL,
304304+ granted BOOLEAN NOT NULL DEFAULT TRUE,
305305+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
306306+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
307307+ UNIQUE(did, client_id, scope)
308308+);
309309+CREATE INDEX idx_oauth_scope_pref_lookup ON oauth_scope_preference(did, client_id);
310310+CREATE TABLE user_totp (
311311+ did TEXT PRIMARY KEY REFERENCES users(did) ON DELETE CASCADE,
312312+ secret_encrypted BYTEA NOT NULL,
313313+ encryption_version INTEGER NOT NULL DEFAULT 1,
314314+ verified BOOLEAN NOT NULL DEFAULT FALSE,
315315+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
316316+ last_used TIMESTAMPTZ
317317+);
318318+CREATE TABLE backup_codes (
319319+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
320320+ did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
321321+ code_hash TEXT NOT NULL,
322322+ used_at TIMESTAMPTZ,
323323+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
324324+);
325325+CREATE INDEX idx_backup_codes_did ON backup_codes(did);
326326+CREATE TABLE passkeys (
327327+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
328328+ did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
329329+ credential_id BYTEA NOT NULL UNIQUE,
330330+ public_key BYTEA NOT NULL,
331331+ sign_count INTEGER NOT NULL DEFAULT 0,
332332+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
333333+ last_used TIMESTAMPTZ,
334334+ friendly_name TEXT,
335335+ aaguid BYTEA,
336336+ transports TEXT[]
337337+);
338338+CREATE INDEX idx_passkeys_did ON passkeys(did);
339339+CREATE TABLE webauthn_challenges (
340340+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
341341+ did TEXT NOT NULL,
342342+ challenge BYTEA NOT NULL,
343343+ challenge_type TEXT NOT NULL,
344344+ state_json TEXT NOT NULL,
345345+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
346346+ expires_at TIMESTAMPTZ NOT NULL
347347+);
348348+CREATE INDEX idx_webauthn_challenges_did ON webauthn_challenges(did);
-15
migrations/20251213_performance_indexes.sql
···11-CREATE INDEX IF NOT EXISTS idx_records_repo_collection
22- ON records(repo_id, collection);
33-CREATE INDEX IF NOT EXISTS idx_records_repo_collection_created
44- ON records(repo_id, collection, created_at DESC);
55-CREATE INDEX IF NOT EXISTS idx_users_email
66- ON users(email)
77- WHERE email IS NOT NULL;
88-CREATE INDEX IF NOT EXISTS idx_blobs_created_by_user
99- ON blobs(created_by_user, created_at DESC);
1010-CREATE INDEX IF NOT EXISTS idx_repo_seq_did_seq
1111- ON repo_seq(did, seq DESC);
1212-CREATE INDEX IF NOT EXISTS idx_app_passwords_user_id
1313- ON app_passwords(user_id);
1414-CREATE INDEX IF NOT EXISTS idx_invite_codes_created_by
1515- ON invite_codes(created_by_user);
-1
migrations/20251214_add_prev_data_cid.sql
···11-ALTER TABLE repo_seq ADD COLUMN IF NOT EXISTS prev_data_cid TEXT;
···11-ALTER TABLE repo_seq ADD COLUMN IF NOT EXISTS handle TEXT;
22-ALTER TABLE repo_seq ADD COLUMN IF NOT EXISTS active BOOLEAN;
33-ALTER TABLE repo_seq ADD COLUMN IF NOT EXISTS status TEXT;
-12
migrations/20251216_add_channel_verification.sql
···11-ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'channel_verification';
22-33-CREATE TABLE IF NOT EXISTS channel_verifications (
44- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
55- channel notification_channel NOT NULL,
66- code TEXT NOT NULL,
77- expires_at TIMESTAMPTZ NOT NULL,
88- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
99- PRIMARY KEY (user_id, channel)
1010-);
1111-1212-CREATE INDEX IF NOT EXISTS idx_channel_verifications_expires ON channel_verifications(expires_at);
···11-DO $$
22-BEGIN
33- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'email_confirmed') THEN
44- ALTER TABLE users RENAME COLUMN email_confirmed TO email_verified;
55- END IF;
66-END $$;
···11-DO $$
22-BEGIN
33- IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'notification_channel') THEN
44- ALTER TYPE notification_channel RENAME TO comms_channel;
55- END IF;
66- IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'notification_status') THEN
77- ALTER TYPE notification_status RENAME TO comms_status;
88- END IF;
99- IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'notification_type') THEN
1010- ALTER TYPE notification_type RENAME TO comms_type;
1111- END IF;
1212- IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'notification_queue') THEN
1313- ALTER TABLE notification_queue RENAME TO comms_queue;
1414- END IF;
1515- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'comms_queue' AND column_name = 'notification_type') THEN
1616- ALTER TABLE comms_queue RENAME COLUMN notification_type TO comms_type;
1717- END IF;
1818- IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_notification_queue_status_scheduled') THEN
1919- ALTER INDEX idx_notification_queue_status_scheduled RENAME TO idx_comms_queue_status_scheduled;
2020- END IF;
2121- IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_notification_queue_user_id') THEN
2222- ALTER INDEX idx_notification_queue_user_id RENAME TO idx_comms_queue_user_id;
2323- END IF;
2424- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'preferred_notification_channel') THEN
2525- ALTER TABLE users RENAME COLUMN preferred_notification_channel TO preferred_comms_channel;
2626- END IF;
2727-END $$;
-12
migrations/20251221_oauth_scope_preferences.sql
···11-CREATE TABLE oauth_scope_preference (
22- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
33- did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
44- client_id TEXT NOT NULL,
55- scope TEXT NOT NULL,
66- granted BOOLEAN NOT NULL DEFAULT TRUE,
77- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
88- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
99- UNIQUE(did, client_id, scope)
1010-);
1111-1212-CREATE INDEX idx_oauth_scope_pref_lookup ON oauth_scope_preference(did, client_id);
···11-CREATE TABLE user_totp (
22- did TEXT PRIMARY KEY REFERENCES users(did) ON DELETE CASCADE,
33- secret_encrypted BYTEA NOT NULL,
44- encryption_version INTEGER NOT NULL DEFAULT 1,
55- verified BOOLEAN NOT NULL DEFAULT FALSE,
66- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
77- last_used TIMESTAMPTZ
88-);
99-1010-CREATE TABLE backup_codes (
1111- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1212- did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
1313- code_hash TEXT NOT NULL,
1414- used_at TIMESTAMPTZ,
1515- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1616-);
1717-CREATE INDEX idx_backup_codes_did ON backup_codes(did);
1818-1919-CREATE TABLE passkeys (
2020- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
2121- did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
2222- credential_id BYTEA NOT NULL UNIQUE,
2323- public_key BYTEA NOT NULL,
2424- sign_count INTEGER NOT NULL DEFAULT 0,
2525- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
2626- last_used TIMESTAMPTZ,
2727- friendly_name TEXT,
2828- aaguid BYTEA,
2929- transports TEXT[]
3030-);
3131-CREATE INDEX idx_passkeys_did ON passkeys(did);
3232-3333-CREATE TABLE webauthn_challenges (
3434- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
3535- did TEXT NOT NULL,
3636- challenge BYTEA NOT NULL,
3737- challenge_type TEXT NOT NULL,
3838- state_json TEXT NOT NULL,
3939- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4040- expires_at TIMESTAMPTZ NOT NULL
4141-);
4242-CREATE INDEX idx_webauthn_challenges_did ON webauthn_challenges(did);
+9-3
src/api/admin/account/update.rs
···6767 Json(input): Json<UpdateAccountHandleInput>,
6868) -> Response {
6969 let did = input.did.trim();
7070- let handle = input.handle.trim();
7171- if did.is_empty() || handle.is_empty() {
7070+ let input_handle = input.handle.trim();
7171+ if did.is_empty() || input_handle.is_empty() {
7272 return (
7373 StatusCode::BAD_REQUEST,
7474 Json(json!({"error": "InvalidRequest", "message": "did and handle are required"})),
7575 )
7676 .into_response();
7777 }
7878- if !handle
7878+ if !input_handle
7979 .chars()
8080 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')
8181 {
···8787 )
8888 .into_response();
8989 }
9090+ let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
9191+ let handle = if !input_handle.contains('.') {
9292+ format!("{}.{}", input_handle, hostname)
9393+ } else {
9494+ input_handle.to_string()
9595+ };
9096 let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did)
9197 .fetch_optional(&state.db)
9298 .await
···426426 let normalized_username = normalized_username
427427 .strip_prefix('@')
428428 .unwrap_or(normalized_username);
429429- let normalized_username = if let Some(bare_handle) =
430430- normalized_username.strip_suffix(&format!(".{}", pds_hostname))
431431- {
432432- bare_handle.to_string()
429429+ let normalized_username = if normalized_username.contains('@') {
430430+ normalized_username.to_string()
431431+ } else if !normalized_username.contains('.') {
432432+ format!("{}.{}", normalized_username, pds_hostname)
433433 } else {
434434 normalized_username.to_string()
435435 };
···15851585 Query(query): Query<CheckPasskeysQuery>,
15861586) -> Response {
15871587 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
15881588- let normalized_identifier = query.identifier.trim();
15891589- let normalized_identifier = normalized_identifier
15901590- .strip_prefix('@')
15911591- .unwrap_or(normalized_identifier);
15921592- let normalized_identifier = if let Some(bare_handle) =
15931593- normalized_identifier.strip_suffix(&format!(".{}", pds_hostname))
15941594- {
15951595- bare_handle.to_string()
15881588+ let identifier = query.identifier.trim();
15891589+ let identifier = identifier.strip_prefix('@').unwrap_or(identifier);
15901590+ let normalized_identifier = if identifier.contains('@') || identifier.starts_with("did:") {
15911591+ identifier.to_string()
15921592+ } else if !identifier.contains('.') {
15931593+ format!("{}.{}", identifier.to_lowercase(), pds_hostname)
15961594 } else {
15971597- normalized_identifier.to_string()
15951595+ identifier.to_lowercase()
15981596 };
1599159716001598 let user = sqlx::query!(
···16951693 let normalized_username = normalized_username
16961694 .strip_prefix('@')
16971695 .unwrap_or(normalized_username);
16981698- let normalized_username = if let Some(bare_handle) =
16991699- normalized_username.strip_suffix(&format!(".{}", pds_hostname))
17001700- {
17011701- bare_handle.to_string()
16961696+ let normalized_username = if normalized_username.contains('@') {
16971697+ normalized_username.to_string()
16981698+ } else if !normalized_username.contains('.') {
16991699+ format!("{}.{}", normalized_username, pds_hostname)
17021700 } else {
17031701 normalized_username.to_string()
17041702 };
+30-29
tests/email_update.rs
···1717 base_url: &str,
1818 handle: &str,
1919 email: &str,
2020-) -> String {
2020+) -> (String, String) {
2121 let res = client
2222 .post(format!(
2323 "{}/xrpc/com.atproto.server.createAccount",
···3333 .expect("Failed to create account");
3434 assert_eq!(res.status(), StatusCode::OK);
3535 let body: Value = res.json().await.expect("Invalid JSON");
3636- let did = body["did"].as_str().expect("No did");
3737- common::verify_new_account(client, did).await
3636+ let did = body["did"].as_str().expect("No did").to_string();
3737+ let jwt = common::verify_new_account(client, &did).await;
3838+ (jwt, did)
3839}
39404041#[tokio::test]
···4445 let pool = get_pool().await;
4546 let handle = format!("emailup_{}", uuid::Uuid::new_v4());
4647 let email = format!("{}@example.com", handle);
4747- let access_jwt = create_verified_account(&client, &base_url, &handle, &email).await;
4848+ let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
4849 let new_email = format!("new_{}@example.com", handle);
4950 let res = client
5051 .post(format!(
···6162 assert_eq!(body["tokenRequired"], true);
62636364 let verification = sqlx::query!(
6464- "SELECT pending_identifier, code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
6565- handle
6565+ "SELECT pending_identifier, code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
6666+ did
6667 )
6768 .fetch_one(&pool)
6869 .await
···8485 .await
8586 .expect("Failed to confirm email");
8687 assert_eq!(res.status(), StatusCode::OK);
8787- let user = sqlx::query!("SELECT email FROM users WHERE handle = $1", handle)
8888+ let user = sqlx::query!("SELECT email FROM users WHERE did = $1", did)
8889 .fetch_one(&pool)
8990 .await
9091 .expect("User not found");
9192 assert_eq!(user.email, Some(new_email));
92939394 let verification = sqlx::query!(
9494- "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
9595- handle
9595+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
9696+ did
9697 )
9798 .fetch_optional(&pool)
9899 .await
···106107 let base_url = common::base_url().await;
107108 let handle1 = format!("emailup_taken1_{}", uuid::Uuid::new_v4());
108109 let email1 = format!("{}@example.com", handle1);
109109- let _ = create_verified_account(&client, &base_url, &handle1, &email1).await;
110110+ let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
110111 let handle2 = format!("emailup_taken2_{}", uuid::Uuid::new_v4());
111112 let email2 = format!("{}@example.com", handle2);
112112- let access_jwt2 = create_verified_account(&client, &base_url, &handle2, &email2).await;
113113+ let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
113114 let res = client
114115 .post(format!(
115116 "{}/xrpc/com.atproto.server.requestEmailUpdate",
···131132 let base_url = common::base_url().await;
132133 let handle = format!("emailup_inv_{}", uuid::Uuid::new_v4());
133134 let email = format!("{}@example.com", handle);
134134- let access_jwt = create_verified_account(&client, &base_url, &handle, &email).await;
135135+ let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
135136 let new_email = format!("new_{}@example.com", handle);
136137 let res = client
137138 .post(format!(
···166167 let pool = get_pool().await;
167168 let handle = format!("emailup_wrong_{}", uuid::Uuid::new_v4());
168169 let email = format!("{}@example.com", handle);
169169- let access_jwt = create_verified_account(&client, &base_url, &handle, &email).await;
170170+ let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
170171 let new_email = format!("new_{}@example.com", handle);
171172 let res = client
172173 .post(format!(
···180181 .expect("Failed to request email update");
181182 assert_eq!(res.status(), StatusCode::OK);
182183 let verification = sqlx::query!(
183183- "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
184184- handle
184184+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
185185+ did
185186 )
186187 .fetch_one(&pool)
187188 .await
···209210 let pool = get_pool().await;
210211 let handle = format!("emailup_direct_{}", uuid::Uuid::new_v4());
211212 let email = format!("{}@example.com", handle);
212212- let access_jwt = create_verified_account(&client, &base_url, &handle, &email).await;
213213+ let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
213214 let new_email = format!("direct_{}@example.com", handle);
214215 let res = client
215216 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
···219220 .await
220221 .expect("Failed to update email");
221222 assert_eq!(res.status(), StatusCode::OK);
222222- let user = sqlx::query!("SELECT email FROM users WHERE handle = $1", handle)
223223+ let user = sqlx::query!("SELECT email FROM users WHERE did = $1", did)
223224 .fetch_one(&pool)
224225 .await
225226 .expect("User not found");
···232233 let base_url = common::base_url().await;
233234 let handle = format!("emailup_same_{}", uuid::Uuid::new_v4());
234235 let email = format!("{}@example.com", handle);
235235- let access_jwt = create_verified_account(&client, &base_url, &handle, &email).await;
236236+ let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
236237 let res = client
237238 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
238239 .bearer_auth(&access_jwt)
···253254 let base_url = common::base_url().await;
254255 let handle = format!("emailup_token_{}", uuid::Uuid::new_v4());
255256 let email = format!("{}@example.com", handle);
256256- let access_jwt = create_verified_account(&client, &base_url, &handle, &email).await;
257257+ let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
257258 let new_email = format!("pending_{}@example.com", handle);
258259 let res = client
259260 .post(format!(
···285286 let pool = get_pool().await;
286287 let handle = format!("emailup_valid_{}", uuid::Uuid::new_v4());
287288 let email = format!("{}@example.com", handle);
288288- let access_jwt = create_verified_account(&client, &base_url, &handle, &email).await;
289289+ let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
289290 let new_email = format!("valid_{}@example.com", handle);
290291 let res = client
291292 .post(format!(
···299300 .expect("Failed to request email update");
300301 assert_eq!(res.status(), StatusCode::OK);
301302 let verification = sqlx::query!(
302302- "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
303303- handle
303303+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
304304+ did
304305 )
305306 .fetch_one(&pool)
306307 .await
···317318 .await
318319 .expect("Failed to update email");
319320 assert_eq!(res.status(), StatusCode::OK);
320320- let user = sqlx::query!("SELECT email FROM users WHERE handle = $1", handle)
321321+ let user = sqlx::query!("SELECT email FROM users WHERE did = $1", did)
321322 .fetch_one(&pool)
322323 .await
323324 .expect("User not found");
324325 assert_eq!(user.email, Some(new_email));
325326 let verification = sqlx::query!(
326326- "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
327327- handle
327327+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
328328+ did
328329 )
329330 .fetch_optional(&pool)
330331 .await
···338339 let base_url = common::base_url().await;
339340 let handle = format!("emailup_badtok_{}", uuid::Uuid::new_v4());
340341 let email = format!("{}@example.com", handle);
341341- let access_jwt = create_verified_account(&client, &base_url, &handle, &email).await;
342342+ let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
342343 let new_email = format!("badtok_{}@example.com", handle);
343344 let res = client
344345 .post(format!(
···372373 let base_url = common::base_url().await;
373374 let handle1 = format!("emailup_dup1_{}", uuid::Uuid::new_v4());
374375 let email1 = format!("{}@example.com", handle1);
375375- let _ = create_verified_account(&client, &base_url, &handle1, &email1).await;
376376+ let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
376377 let handle2 = format!("emailup_dup2_{}", uuid::Uuid::new_v4());
377378 let email2 = format!("{}@example.com", handle2);
378378- let access_jwt2 = create_verified_account(&client, &base_url, &handle2, &email2).await;
379379+ let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await;
379380 let res = client
380381 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
381382 .bearer_auth(&access_jwt2)
···412413 let base_url = common::base_url().await;
413414 let handle = format!("emailup_fmt_{}", uuid::Uuid::new_v4());
414415 let email = format!("{}@example.com", handle);
415415- let access_jwt = create_verified_account(&client, &base_url, &handle, &email).await;
416416+ let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
416417 let res = client
417418 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
418419 .bearer_auth(&access_jwt)
+5-4
tests/identity.rs
···88#[tokio::test]
99async fn test_resolve_handle_success() {
1010 let client = client();
1111- let handle = format!("resolvetest_{}", uuid::Uuid::new_v4());
1111+ let short_handle = format!("resolvetest_{}", uuid::Uuid::new_v4());
1212 let payload = json!({
1313- "handle": handle,
1414- "email": format!("{}@example.com", handle),
1313+ "handle": short_handle,
1414+ "email": format!("{}@example.com", short_handle),
1515 "password": "password"
1616 });
1717 let res = client
···2626 assert_eq!(res.status(), StatusCode::OK);
2727 let body: Value = res.json().await.expect("Invalid JSON");
2828 let did = body["did"].as_str().expect("No DID").to_string();
2929- let params = [("handle", handle.as_str())];
2929+ let full_handle = body["handle"].as_str().expect("No handle in response").to_string();
3030+ let params = [("handle", full_handle.as_str())];
3031 let res = client
3132 .get(format!(
3233 "{}/xrpc/com.atproto.identity.resolveHandle",