Skip to content
1,331 linesCodeBlameRaw
1//! A person's email addresses: adding, confirming, choosing the primary and
2//! the backup, removing, keeping them private, and finding whose an address
3//! is.
4//!
5//! `user_emails` holds every address. `users.primary_email_id` names the
6//! primary, and `users.email` and `users.email_verified_at` are kept as a
7//! copy of it (other code reads them, and "the account is confirmed" means
8//! its primary is). Every change to the primary here writes all three.
9//!
10//! A confirmed address belongs to one account: a unique index on confirmed
11//! rows makes sure of it. Unconfirmed rows may repeat across accounts; the
12//! first account to follow its confirmation link keeps the address, in one
13//! transaction that confirms its row only if nobody else's is confirmed,
14//! and then drops everyone else's unconfirmed row for it. An account whose
15//! primary was dropped that way (it never confirmed it) is left without one
16//! until it confirms another address, which becomes primary on its own.
17//!
18//! Sensitive changes (adding, removing, primary, backup) need proof that it
19//! is the person (`security.rs`), are written to their security log, are
20//! announced as `user.email_*` events, and are told to every confirmed
21//! address, the removed one included.
22//!
23//! A confirmation email carries a six-digit code and a link, either one
24//! enough. Both live in one `email_tokens` row, bound to the account and
25//! the address: the link's token as its id (a SHA-256), the code as an
26//! HMAC under IDENTITY_KEY ([`crypto::code_hash`]). Using either deletes
27//! the row, so the other stops working too, and sending another email for
28//! the address deletes the rows before it. Both work for
29//! [`CONFIRM_TTL_SECONDS`]. Wrong codes are throttled per account and per
30//! client (throttle.rs).
31//!
32//! An account with no confirmed primary is pending: the site, the API and
33//! git let it do nothing but confirm its address, change it, or sign out.
34//! The invite it signed up with was spent then, and what it gives (its
35//! workspace) is applied in the same transaction that confirms the address
36//! (invites.rs, `apply_invite_statements`).
37
38use std::collections::HashMap;
39
40use g1t_contracts::accounts::*;
41use g1t_contracts::events::UserEmailChanged;
42use g1t_contracts::identity::{UserArgs, UsernameArgs};
43use g1t_contracts::time::{SQL_NOW, rfc3339, sql_after};
44use g1t_contracts::{FailureCode, Outcome, new_id};
45use g1t_kit::now_ms;
46use serde::Deserialize;
47use worker::Result;
48use worker::wasm_bindgen::JsValue;
49
50use crate::email::Confirming;
51use crate::invites::AwaitingJoin;
52use crate::security::{PEOPLE_ONLY, is_person};
53use crate::throttle::{self, CODE_ACCOUNT, CODE_CLIENT, CODE_THROTTLED, CONFIRM_ACCOUNT};
54use crate::{Identity, crypto, email};
55
56/// The one answer to a code that does not confirm anything: wrong, used,
57/// expired, or for an address no longer on the account.
58pub const WRONG_CODE: &str = "That code is not right, or it has expired. Check the latest email from g1t, or send a new code.";
59
60/// The answer when a confirmation email was sent less than a minute ago.
61pub const SENT_RECENTLY: &str = "We sent an email less than a minute ago. Check your inbox, then try again.";
62
63/// One row of `user_emails`.
64#[derive(Clone, Debug, Deserialize)]
65pub struct EmailRow {
66 pub id: String,
67 pub email: String,
68 pub display: String,
69 pub verified_at: Option<String>,
70 pub sent_at: Option<String>,
71 pub created_at: String,
72}
73
74/// What `users` says about a person's addresses.
75#[derive(Debug, Deserialize)]
76struct AccountRow {
77 id: String,
78 username: String,
79 primary_email_id: Option<String>,
80 backup_email_id: Option<String>,
81 private_email: u8,
82 block_private_pushes: u8,
83 #[serde(default)]
84 created_at: String,
85}
86
87const ACCOUNT_COLUMNS: &str =
88 "id, username, primary_email_id, backup_email_id, private_email, block_private_pushes, created_at";
89
90/// The order addresses are shown in: the primary, confirmed ones, the rest;
91/// oldest first within each.
92fn sorted(mut rows: Vec<EmailRow>, primary: Option<&str>) -> Vec<EmailRow> {
93 rows.sort_by(|a, b| {
94 let rank = |row: &EmailRow| (Some(row.id.as_str()) != primary, row.verified_at.is_none());
95 rank(a).cmp(&rank(b)).then_with(|| a.created_at.cmp(&b.created_at)).then_with(|| a.id.cmp(&b.id))
96 });
97 rows
98}
99
100fn states(rows: &[EmailRow], primary: Option<&str>) -> Vec<EmailState> {
101 rows.iter()
102 .map(|row| EmailState {
103 email: row.email.clone(),
104 verified: row.verified_at.is_some(),
105 primary: Some(row.id.as_str()) == primary,
106 })
107 .collect()
108}
109
110/// The address commits g1t makes for a person carry: their noreply address
111/// while they keep their address private or have no confirmed primary.
112fn commit_email(private: bool, primary: Option<&EmailRow>, noreply: &str) -> String {
113 match primary {
114 Some(row) if !private && row.verified_at.is_some() => row.email.clone(),
115 _ => noreply.to_owned(),
116 }
117}
118
119fn view(account: &AccountRow, rows: Vec<EmailRow>) -> AccountEmails {
120 let primary = account.primary_email_id.as_deref();
121 let rows = sorted(rows, primary);
122 let noreply = noreply_address(&account.id, &account.username);
123 let private = account.private_email != 0;
124 let commit = commit_email(private, rows.iter().find(|row| Some(row.id.as_str()) == primary), &noreply);
125 AccountEmails {
126 emails: rows
127 .into_iter()
128 .map(|row| AccountEmail {
129 primary: Some(row.id.as_str()) == primary,
130 backup: Some(row.id.as_str()) == account.backup_email_id.as_deref(),
131 verified: row.verified_at.is_some(),
132 email: row.display,
133 created_at: row.created_at,
134 verified_at: row.verified_at,
135 })
136 .collect(),
137 private_email: private,
138 block_private_pushes: account.block_private_pushes != 0,
139 noreply,
140 commit_email: commit,
141 limit: MAX_EMAILS as u32,
142 }
143}
144
145/// Whether pushes by this person are checked for their addresses: only
146/// while the address is private and they asked for pushes to be blocked.
147fn guards_pushes(account: &AccountRow) -> bool {
148 account.private_email != 0 && account.block_private_pushes != 0
149}
150
151/// Whether a confirmation link may be sent again to an address last sent
152/// one at `sent_at`, at `now_ms`.
153pub fn may_resend(sent_at: Option<&str>, now_ms: u64) -> bool {
154 !is_recent(sent_at, now_ms, RESEND_SECONDS)
155}
156
157impl Identity {
158 async fn account_row(&self, user_id: &str) -> Result<Option<AccountRow>> {
159 self.db
160 .prepare(format!("SELECT {ACCOUNT_COLUMNS} FROM users WHERE id = ?"))
161 .bind(&[user_id.into()])?
162 .first::<AccountRow>(None)
163 .await
164 }
165
166 pub async fn email_rows(&self, user_id: &str) -> Result<Vec<EmailRow>> {
167 self.db
168 .prepare(
169 "SELECT id, email, display, verified_at, sent_at, created_at FROM user_emails
170 WHERE user_id = ? ORDER BY created_at, id",
171 )
172 .bind(&[user_id.into()])?
173 .all()
174 .await?
175 .results::<EmailRow>()
176 }
177
178 async fn emails_view(&self, user_id: &str) -> Result<Outcome<AccountEmails>> {
179 let Some(account) = self.account_row(user_id).await? else {
180 return Ok(Outcome::fail(FailureCode::NotFound, "No such account."));
181 };
182 let rows = self.email_rows(user_id).await?;
183 Ok(Outcome::Ok(view(&account, rows)))
184 }
185
186 /// A person's confirmed addresses, lowercased, the primary first.
187 pub async fn verified_emails(&self, user_id: &str) -> Result<Vec<String>> {
188 let account = self.account_row(user_id).await?;
189 let primary = account.as_ref().and_then(|account| account.primary_email_id.as_deref());
190 Ok(sorted(self.email_rows(user_id).await?, primary)
191 .into_iter()
192 .filter(|row| row.verified_at.is_some())
193 .map(|row| row.email)
194 .collect())
195 }
196
197 /// The account that has confirmed `email`, by id. The one helper every
198 /// "does this address belong to someone" question goes through (signing
199 /// in with GitHub, invites, password resets, signing in by email).
200 pub async fn user_with_verified_email(&self, email: &str) -> Result<Option<String>> {
201 #[derive(Deserialize)]
202 struct Owner {
203 user_id: String,
204 }
205 let Some(email) = normalize_email(email) else {
206 return Ok(None);
207 };
208 Ok(self
209 .db
210 .prepare("SELECT user_id FROM user_emails WHERE email = ? AND verified_at IS NOT NULL")
211 .bind(&[email.as_str().into()])?
212 .first::<Owner>(None)
213 .await?
214 .map(|owner| owner.user_id))
215 }
216
217 /// Whether `email` is any account's: confirmed, or the unconfirmed
218 /// primary a new account signed up with.
219 pub async fn email_in_use(&self, email: &str) -> Result<bool> {
220 let Some(email) = normalize_email(email) else {
221 return Ok(false);
222 };
223 let found = self
224 .db
225 .prepare(
226 "SELECT e.id FROM user_emails e JOIN users u ON u.id = e.user_id
227 WHERE e.email = ?1 AND (e.verified_at IS NOT NULL OR u.primary_email_id = e.id) LIMIT 1",
228 )
229 .bind(&[email.as_str().into()])?
230 .first::<serde_json::Value>(None)
231 .await?;
232 Ok(found.is_some())
233 }
234
235 /// `list_emails`.
236 pub async fn list_emails(&self, a: UserArgs) -> Result<Outcome<AccountEmails>> {
237 if !is_person(&a.user) {
238 return Ok(Outcome::fail(FailureCode::Forbidden, PEOPLE_ONLY));
239 }
240 self.emails_view(&a.user.id).await
241 }
242
243 /// The key confirmation codes are kept under: IDENTITY_KEY, or none in
244 /// a development setup without one.
245 fn code_key(&self) -> Vec<u8> {
246 self.env.secret("IDENTITY_KEY").map(|key| key.to_string().into_bytes()).unwrap_or_default()
247 }
248
249 /// Stores a new code and link for one address, ending any sent before
250 /// for it, and emails them.
251 async fn send_confirmation(&self, user_id: &str, username: &str, row: &EmailRow, confirming: Confirming) -> Result<()> {
252 let token = crypto::random_hex(32);
253 let code = crypto::random_digits(CONFIRM_CODE_DIGITS);
254 let id = crypto::sha256_hex(&token);
255 let code_hash = crypto::code_hash(&self.code_key(), &id, &code);
256 self.db
257 .batch(vec![
258 // A new email ends the code and link of the one before.
259 self.db
260 .prepare("DELETE FROM email_tokens WHERE user_id = ? AND kind = 'verify' AND email_id = ?")
261 .bind(&[user_id.into(), row.id.as_str().into()])?,
262 self.db
263 .prepare(format!(
264 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id, code_hash)
265 VALUES (?, ?, 'verify', {}, ?, ?)",
266 sql_after(CONFIRM_TTL_SECONDS)
267 ))
268 .bind(&[id.as_str().into(), user_id.into(), row.id.as_str().into(), code_hash.as_str().into()])?,
269 self.db
270 .prepare(format!("UPDATE user_emails SET sent_at = {SQL_NOW} WHERE id = ?"))
271 .bind(&[row.id.as_str().into()])?,
272 ])
273 .await?;
274 email::send_confirmation(&self.env, &row.display, username, confirming, &token, &code).await
275 }
276
277 /// Sends a new account its first code and link, to its primary.
278 pub async fn send_primary_confirmation(&self, user_id: &str, username: &str) -> Result<()> {
279 let Some(account) = self.account_row(user_id).await? else {
280 return Ok(());
281 };
282 let rows = self.email_rows(user_id).await?;
283 let Some(row) = rows.iter().find(|row| Some(row.id.as_str()) == account.primary_email_id.as_deref()) else {
284 return Ok(());
285 };
286 if row.verified_at.is_some() {
287 return Ok(());
288 }
289 self.send_confirmation(user_id, username, row, Confirming::NewAccount).await
290 }
291
292 /// `add_email`.
293 pub async fn add_email(&self, a: AccountEmailArgs) -> Result<Outcome<AccountEmails>> {
294 if !is_person(&a.user) {
295 return Ok(Outcome::fail(FailureCode::Forbidden, PEOPLE_ONLY));
296 }
297 let typed = a.email.trim();
298 let Some(email) = normalize_email(typed) else {
299 return Ok(Outcome::fail(FailureCode::Invalid, "Enter a valid email address."));
300 };
301 if parse_noreply(&email).is_some() || email.ends_with("@users.g1t.sh") {
302 return Ok(Outcome::fail(FailureCode::Invalid, "That is a g1t noreply address; add an address you receive mail at."));
303 }
304 if let Some(refusal) = self.proof(&a.user.id, &a.reauth).await?.refusal() {
305 return Ok(refusal);
306 }
307 let rows = self.email_rows(&a.user.id).await?;
308 if let Some(row) = rows.iter().find(|row| row.email == email) {
309 if row.verified_at.is_some() {
310 return Ok(Outcome::fail(FailureCode::Conflict, "That address is already on your account."));
311 }
312 // Added before and not confirmed: adding it again sends the link again.
313 if may_resend(row.sent_at.as_deref(), now_ms()) && self.allow(CONFIRM_ACCOUNT, &a.user.id).await? {
314 self.send_confirmation(&a.user.id, &a.user.username, row, Confirming::AddedAddress).await?;
315 }
316 return self.emails_view(&a.user.id).await;
317 }
318 if rows.len() >= MAX_EMAILS {
319 return Ok(Outcome::fail(
320 FailureCode::Invalid,
321 format!("An account can have {MAX_EMAILS} addresses. Remove one first."),
322 ));
323 }
324 if self.user_with_verified_email(&email).await?.is_some() {
325 return Ok(Outcome::fail(FailureCode::Conflict, "That address is confirmed on another g1t account."));
326 }
327 let now = now_ms();
328 let row = EmailRow {
329 id: new_id("eml", now),
330 email: email.clone(),
331 display: typed.to_owned(),
332 verified_at: None,
333 sent_at: None,
334 created_at: rfc3339(now),
335 };
336 let inserted = self
337 .db
338 .prepare(
339 "INSERT INTO user_emails (id, user_id, email, display, created_at)
340 SELECT ?1, ?2, ?3, ?4, ?5
341 WHERE (SELECT count(*) FROM user_emails WHERE user_id = ?2) < ?6",
342 )
343 .bind(&[
344 row.id.as_str().into(),
345 a.user.id.as_str().into(),
346 row.email.as_str().into(),
347 row.display.as_str().into(),
348 row.created_at.as_str().into(),
349 (MAX_EMAILS as f64).into(),
350 ])?
351 .run()
352 .await;
353 if let Err(error) = inserted {
354 // The same address added twice at once.
355 if error.to_string().contains("UNIQUE") {
356 return self.emails_view(&a.user.id).await;
357 }
358 return Err(error);
359 }
360 if !self.allow(CONFIRM_ACCOUNT, &a.user.id).await? {
361 worker::console_log!("confirmation email held back: too many this hour");
362 } else if let Err(error) = self.send_confirmation(&a.user.id, &a.user.username, &row, Confirming::AddedAddress).await {
363 worker::console_error!("confirmation email failed: {error}");
364 }
365 self.log_security(&a.user.id, "email_added", Some(&row.display), None).await;
366 self.tell_addresses(&a.user.id, &a.user.username, &format!("{} was added", row.display), None).await;
367 self.announce_email("user.email_added", &a.user.id, false).await;
368 self.emails_view(&a.user.id).await
369 }
370
371 /// `resend_email_verification`.
372 pub async fn resend_email_verification(&self, a: AccountEmailArgs) -> Result<Outcome<bool>> {
373 if !is_person(&a.user) {
374 return Ok(Outcome::fail(FailureCode::Forbidden, PEOPLE_ONLY));
375 }
376 let email = normalize_email(&a.email).unwrap_or_default();
377 let rows = self.email_rows(&a.user.id).await?;
378 let Some(row) = rows.iter().find(|row| row.email == email) else {
379 return Ok(Outcome::fail(FailureCode::NotFound, "That address is not on your account."));
380 };
381 if row.verified_at.is_some() {
382 return Ok(Outcome::fail(FailureCode::Conflict, "That address is already confirmed."));
383 }
384 if !may_resend(row.sent_at.as_deref(), now_ms()) {
385 return Ok(Outcome::fail(FailureCode::Conflict, SENT_RECENTLY));
386 }
387 if !self.allow(CONFIRM_ACCOUNT, &a.user.id).await? {
388 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
389 }
390 let confirming = if self.account_confirmed(&a.user.id).await? { Confirming::AddedAddress } else { Confirming::NewAccount };
391 self.send_confirmation(&a.user.id, &a.user.username, row, confirming).await?;
392 Ok(Outcome::Ok(true))
393 }
394
395 /// Whether the account has a confirmed primary: whether it is past the
396 /// confirmation page.
397 pub async fn account_confirmed(&self, user_id: &str) -> Result<bool> {
398 let Some(account) = self.account_row(user_id).await? else {
399 return Ok(false);
400 };
401 Ok(self
402 .email_rows(user_id)
403 .await?
404 .iter()
405 .any(|row| Some(row.id.as_str()) == account.primary_email_id.as_deref() && row.verified_at.is_some()))
406 }
407
408 /// The confirmation page's "send a new code": a new code and link for
409 /// the primary of an account that has not confirmed it, or for its
410 /// oldest unconfirmed address when a confirmed account elsewhere took
411 /// its primary. At most one a minute; the ones before stop working.
412 pub async fn resend_primary(&self, user: &g1t_contracts::User) -> Result<Outcome<bool>> {
413 let Some(account) = self.account_row(&user.id).await? else {
414 return Ok(Outcome::fail(FailureCode::NotFound, "No such account."));
415 };
416 let rows = sorted(self.email_rows(&user.id).await?, account.primary_email_id.as_deref());
417 if rows.iter().any(|row| Some(row.id.as_str()) == account.primary_email_id.as_deref() && row.verified_at.is_some()) {
418 return Ok(Outcome::fail(FailureCode::Conflict, "This account's email is already confirmed."));
419 }
420 let Some(row) = rows.iter().find(|row| row.verified_at.is_none()) else {
421 return Ok(Outcome::fail(FailureCode::Conflict, "Add an email address in your settings first."));
422 };
423 if !may_resend(row.sent_at.as_deref(), now_ms()) {
424 return Ok(Outcome::fail(FailureCode::Conflict, SENT_RECENTLY));
425 }
426 self.send_confirmation(&user.id, &account.username, row, Confirming::NewAccount).await?;
427 Ok(Outcome::Ok(true))
428 }
429
430 /// `change_pending_email`: the confirmation page's "wrong address?".
431 /// Only for an account with no confirmed address: its unconfirmed
432 /// addresses are replaced by this one, which becomes the primary and
433 /// gets a new code and link. Needs no password: the account has nothing
434 /// yet that one would protect, and the new address still has to be
435 /// confirmed. Counts against the confirmation emails an hour.
436 pub async fn change_pending_email(&self, a: PendingEmailArgs) -> Result<Outcome<AccountEmails>> {
437 if !is_person(&a.user) {
438 return Ok(Outcome::fail(FailureCode::Forbidden, PEOPLE_ONLY));
439 }
440 let typed = a.email.trim();
441 let Some(email) = normalize_email(typed) else {
442 return Ok(Outcome::fail(FailureCode::Invalid, "Enter a valid email address."));
443 };
444 if parse_noreply(&email).is_some() || email.ends_with("@users.g1t.sh") {
445 return Ok(Outcome::fail(FailureCode::Invalid, "That is a g1t noreply address; use an address you receive mail at."));
446 }
447 let rows = self.email_rows(&a.user.id).await?;
448 if rows.iter().any(|row| row.verified_at.is_some()) {
449 return Ok(Outcome::fail(
450 FailureCode::Conflict,
451 "Your account has a confirmed address already. Change your addresses in your email settings.",
452 ));
453 }
454 if self.user_with_verified_email(&email).await?.is_some() {
455 return Ok(Outcome::fail(FailureCode::Conflict, "That address is confirmed on another g1t account."));
456 }
457 // The address it has already: nothing to change; the page's "send a
458 // new code" sends one.
459 if let [only] = rows.as_slice()
460 && only.email == email
461 {
462 return self.emails_view(&a.user.id).await;
463 }
464 if !self.allow(CONFIRM_ACCOUNT, &a.user.id).await? {
465 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
466 }
467 let now = now_ms();
468 let row = EmailRow {
469 id: new_id("eml", now),
470 email: email.clone(),
471 display: typed.to_owned(),
472 verified_at: None,
473 sent_at: None,
474 created_at: rfc3339(now),
475 };
476 let user = JsValue::from(a.user.id.as_str());
477 // One transaction: every unconfirmed address and its codes go, the
478 // new one comes in as the primary.
479 let changed = self
480 .db
481 .batch(vec![
482 self.db
483 .prepare("DELETE FROM email_tokens WHERE user_id = ? AND kind = 'verify'")
484 .bind(&[user.clone()])?,
485 self.db
486 .prepare("UPDATE users SET primary_email_id = NULL, backup_email_id = NULL, email = NULL, email_verified_at = NULL WHERE id = ?")
487 .bind(&[user.clone()])?,
488 self.db
489 .prepare("DELETE FROM user_emails WHERE user_id = ? AND verified_at IS NULL")
490 .bind(&[user.clone()])?,
491 self.db
492 .prepare("INSERT INTO user_emails (id, user_id, email, display, created_at) VALUES (?, ?, ?, ?, ?)")
493 .bind(&[
494 row.id.as_str().into(),
495 user.clone(),
496 row.email.as_str().into(),
497 row.display.as_str().into(),
498 row.created_at.as_str().into(),
499 ])?,
500 self.db
501 .prepare("UPDATE users SET primary_email_id = ?, email = ? WHERE id = ?")
502 .bind(&[row.id.as_str().into(), row.email.as_str().into(), user.clone()])?,
503 ])
504 .await;
505 if let Err(error) = changed {
506 if error.to_string().contains("UNIQUE") {
507 return Ok(Outcome::fail(FailureCode::Conflict, "That address is already registered."));
508 }
509 return Err(error);
510 }
511 self.log_security(&a.user.id, "email_changed_before_confirming", Some(&row.display), None).await;
512 if let Err(error) = self.send_confirmation(&a.user.id, &a.user.username, &row, Confirming::NewAccount).await {
513 worker::console_error!("confirmation email failed: {error}");
514 }
515 self.announce_email("user.primary_email_changed", &a.user.id, false).await;
516 self.emails_view(&a.user.id).await
517 }
518
519 /// `confirm_email_code`: the code from a confirmation email, typed by
520 /// the signed-in person it was sent to. Every outstanding code of the
521 /// account is compared, each in constant time; wrong ones are counted
522 /// against the account and the client (throttle.rs). A right one is
523 /// used up with its link, then confirms the address it was sent to.
524 pub async fn confirm_email_code(&self, a: ConfirmEmailCodeArgs) -> Result<Outcome<EmailConfirmed>> {
525 #[derive(Deserialize)]
526 struct Sent {
527 id: String,
528 email_id: Option<String>,
529 code_hash: String,
530 expires_at: String,
531 }
532 if !is_person(&a.user) {
533 return Ok(Outcome::fail(FailureCode::Forbidden, PEOPLE_ONLY));
534 }
535 let account_key = throttle::key(CODE_ACCOUNT, &a.user.id);
536 let client_key = a
537 .client
538 .as_deref()
539 .filter(|client| !client.trim().is_empty())
540 .map(|client| throttle::key(CODE_CLIENT, client));
541 // While either key is locked, no code is even compared.
542 let mut locked = self.locked(&account_key).await?;
543 if !locked && let Some(client_key) = &client_key {
544 locked = self.locked(client_key).await?;
545 }
546 if locked {
547 return Ok(Outcome::fail(FailureCode::Conflict, CODE_THROTTLED));
548 }
549 let now = rfc3339(now_ms());
550 let sent: Vec<Sent> = match tidy_confirm_code(&a.code) {
551 Some(_) => self
552 .db
553 .prepare(
554 "SELECT id, email_id, code_hash, expires_at FROM email_tokens
555 WHERE user_id = ? AND kind = 'verify' AND code_hash IS NOT NULL",
556 )
557 .bind(&[a.user.id.as_str().into()])?
558 .all()
559 .await?
560 .results::<Sent>()?
561 .into_iter()
562 .filter(|sent| still_works(&sent.expires_at, &now))
563 .collect(),
564 None => Vec::new(),
565 };
566 let code = tidy_confirm_code(&a.code).unwrap_or_default();
567 let key = self.code_key();
568 let matched = matching_code(&key, &code, sent.iter().map(|sent| (sent.id.as_str(), sent.code_hash.as_str())))
569 .and_then(|index| sent.get(index));
570 // Used once: whoever deletes the row first has it.
571 let used = match matched {
572 Some(sent) => self
573 .db
574 .prepare("DELETE FROM email_tokens WHERE id = ? AND user_id = ? RETURNING id")
575 .bind(&[sent.id.as_str().into(), a.user.id.as_str().into()])?
576 .first::<serde_json::Value>(None)
577 .await?
578 .is_some(),
579 None => false,
580 };
581 let Some(sent) = matched.filter(|_| used) else {
582 self.count(CODE_ACCOUNT, &account_key).await?;
583 if let Some(client_key) = &client_key {
584 self.count(CODE_CLIENT, client_key).await?;
585 }
586 return Ok(Outcome::fail(FailureCode::Invalid, WRONG_CODE));
587 };
588 self.clear(&account_key).await?;
589 // Any other code and link for the same address go too.
590 self.db
591 .prepare("DELETE FROM email_tokens WHERE user_id = ? AND kind = 'verify' AND email_id IS ?")
592 .bind(&[a.user.id.as_str().into(), sent.email_id.as_deref().map_or(JsValue::NULL, Into::into)])?
593 .run()
594 .await?;
595 self.confirm_address(&a.user.id, sent.email_id.as_deref()).await
596 }
597
598 /// Confirms an address after its link was followed or its code typed:
599 /// `email_id`, or the primary for links sent before addresses had ids.
600 /// When this confirms the account, the invite it signed up with is
601 /// applied in the same transaction. Returns what it did, or why the
602 /// address could not be confirmed.
603 pub async fn confirm_address(&self, user_id: &str, email_id: Option<&str>) -> Result<Outcome<EmailConfirmed>> {
604 let Some(account) = self.account_row(user_id).await? else {
605 return Ok(Outcome::fail(FailureCode::Invalid, "This confirmation link is not valid or has expired."));
606 };
607 let Some(email_id) = email_id.map(str::to_owned).or(account.primary_email_id.clone()) else {
608 return Ok(Outcome::fail(FailureCode::Invalid, "This confirmation link is not valid or has expired."));
609 };
610 let rows = self.email_rows(user_id).await?;
611 let Some(row) = rows.into_iter().find(|row| row.id == email_id) else {
612 return Ok(Outcome::fail(
613 FailureCode::Conflict,
614 "That address was confirmed by another g1t account first, or was removed from yours.",
615 ));
616 };
617 if row.verified_at.is_some() {
618 return Ok(Outcome::Ok(EmailConfirmed {
619 username: account.username,
620 email: row.display,
621 verified: self.account_confirmed(user_id).await?,
622 ..EmailConfirmed::default()
623 }));
624 }
625 // The invite the account signed up with, if it waits for this.
626 let awaiting = self.awaiting_invite(user_id).await?;
627 let join = match &awaiting {
628 Some(invite) => Some(self.awaiting_join(invite).await),
629 None => None,
630 };
631 let won = |sql: &str| sql.replace("{WON}", "EXISTS (SELECT 1 FROM user_emails WHERE id = ?1 AND verified_at IS NOT NULL)");
632 let id = JsValue::from(row.id.as_str());
633 let user = JsValue::from(user_id);
634 let address = JsValue::from(row.email.as_str());
635 // One transaction. Confirm this row only if no account has the
636 // address confirmed; then, only if it was, drop everyone else's
637 // claim to it, and make it this account's primary when the account
638 // has no confirmed primary; then, if that confirmed the account,
639 // apply the invite it signed up with.
640 let mut statements = vec![
641 self.db
642 .prepare(format!(
643 "UPDATE user_emails SET verified_at = {SQL_NOW}
644 WHERE id = ?1 AND verified_at IS NULL
645 AND NOT EXISTS (SELECT 1 FROM user_emails WHERE email = ?2 AND verified_at IS NOT NULL)"
646 ))
647 .bind(&[id.clone(), address.clone()])?,
648 self.db
649 .prepare(won(
650 "UPDATE users SET email = NULL, email_verified_at = NULL, primary_email_id = NULL
651 WHERE id <> ?3 AND {WON} AND primary_email_id IN (
652 SELECT id FROM user_emails WHERE email = ?2 AND verified_at IS NULL AND user_id <> ?3)",
653 ))
654 .bind(&[id.clone(), address.clone(), user.clone()])?,
655 self.db
656 .prepare(won(
657 "UPDATE users SET backup_email_id = NULL
658 WHERE id <> ?3 AND {WON} AND backup_email_id IN (
659 SELECT id FROM user_emails WHERE email = ?2 AND verified_at IS NULL AND user_id <> ?3)",
660 ))
661 .bind(&[id.clone(), address.clone(), user.clone()])?,
662 self.db
663 .prepare(won(
664 "DELETE FROM user_emails WHERE email = ?2 AND verified_at IS NULL AND user_id <> ?3 AND {WON}",
665 ))
666 .bind(&[id.clone(), address.clone(), user.clone()])?,
667 self.db
668 .prepare(won(
669 "UPDATE users SET primary_email_id = ?1, email = ?2,
670 email_verified_at = (SELECT verified_at FROM user_emails WHERE id = ?1)
671 WHERE id = ?3 AND {WON} AND (
672 primary_email_id IS NULL OR primary_email_id = ?1
673 OR NOT EXISTS (SELECT 1 FROM user_emails WHERE id = users.primary_email_id AND verified_at IS NOT NULL))",
674 ))
675 .bind(&[id.clone(), address.clone(), user.clone()])?,
676 ];
677 if let (Some(invite), Some(join)) = (&awaiting, &join) {
678 statements.extend(self.apply_invite_statements(user_id, invite, join)?);
679 }
680 self.db.batch(statements).await?;
681 let confirmed = self
682 .email_rows(user_id)
683 .await?
684 .into_iter()
685 .any(|current| current.id == row.id && current.verified_at.is_some());
686 if !confirmed {
687 return Ok(Outcome::fail(
688 FailureCode::Conflict,
689 "That address was confirmed by another g1t account first. Use a different address.",
690 ));
691 }
692 self.log_security(user_id, "email_verified", Some(&row.display), None).await;
693 self.announce_email("user.email_verified", user_id, false).await;
694 let verified = self.account_confirmed(user_id).await?;
695 let (mut joined, mut invite_lapsed) = (None, None);
696 if let (Some(invite), Some(join), true) = (&awaiting, &join, verified) {
697 let user = g1t_contracts::User {
698 id: user_id.to_owned(),
699 username: account.username.clone(),
700 verified: true,
701 ..g1t_contracts::User::default()
702 };
703 joined = self.after_applied(invite, &user, join).await?;
704 invite_lapsed = match join {
705 AwaitingJoin::Lapsed(why) => Some(why.clone()),
706 // Revoked between the read and the transaction.
707 AwaitingJoin::Join { .. } if joined.is_none() => Some(
708 "Your email address is confirmed. The invite you signed up with no longer applies, so it did not join you to a workspace."
709 .to_owned(),
710 ),
711 _ => None,
712 };
713 }
714 Ok(Outcome::Ok(EmailConfirmed {
715 username: account.username,
716 email: row.display,
717 verified,
718 joined,
719 invite_lapsed,
720 }))
721 }
722
723 /// `remove_email`.
724 pub async fn remove_email(&self, a: AccountEmailArgs) -> Result<Outcome<AccountEmails>> {
725 if !is_person(&a.user) {
726 return Ok(Outcome::fail(FailureCode::Forbidden, PEOPLE_ONLY));
727 }
728 let email = normalize_email(&a.email).unwrap_or_default();
729 let Some(account) = self.account_row(&a.user.id).await? else {
730 return Ok(Outcome::fail(FailureCode::NotFound, "No such account."));
731 };
732 let rows = self.email_rows(&a.user.id).await?;
733 if let Some(why) = removal_refusal(&states(&rows, account.primary_email_id.as_deref()), &email) {
734 return Ok(Outcome::fail(FailureCode::Conflict, why));
735 }
736 if let Some(refusal) = self.proof(&a.user.id, &a.reauth).await?.refusal() {
737 return Ok(refusal);
738 }
739 let Some(row) = rows.into_iter().find(|row| row.email == email) else {
740 return self.emails_view(&a.user.id).await;
741 };
742 self.delete_address(&a.user.id, &row).await?;
743 self.log_security(&a.user.id, "email_removed", Some(&row.display), None).await;
744 let removed = row.verified_at.is_some().then_some(row.display.as_str());
745 self.tell_addresses(&a.user.id, &a.user.username, &format!("{} was removed", row.display), removed).await;
746 self.announce_email("user.email_removed", &a.user.id, false).await;
747 self.emails_view(&a.user.id).await
748 }
749
750 /// Deletes an address and anything pointing at it. The caller has made
751 /// sure it is not the primary, or has moved the primary already.
752 async fn delete_address(&self, user_id: &str, row: &EmailRow) -> Result<()> {
753 self.db
754 .batch(vec![
755 self.db
756 .prepare("UPDATE users SET backup_email_id = NULL WHERE id = ? AND backup_email_id = ?")
757 .bind(&[user_id.into(), row.id.as_str().into()])?,
758 self.db
759 .prepare("DELETE FROM email_tokens WHERE user_id = ? AND email_id = ?")
760 .bind(&[user_id.into(), row.id.as_str().into()])?,
761 self.db
762 .prepare("DELETE FROM user_emails WHERE id = ? AND user_id = ?")
763 .bind(&[row.id.as_str().into(), user_id.into()])?,
764 ])
765 .await?;
766 Ok(())
767 }
768
769 /// Makes a confirmed address the primary, keeping `users` in step.
770 async fn set_primary(&self, user_id: &str, row: &EmailRow) -> Result<()> {
771 self.db
772 .prepare(
773 "UPDATE users SET primary_email_id = ?1, email = ?2,
774 email_verified_at = (SELECT verified_at FROM user_emails WHERE id = ?1),
775 backup_email_id = CASE WHEN backup_email_id = ?1 THEN NULL ELSE backup_email_id END
776 WHERE id = ?3 AND EXISTS (SELECT 1 FROM user_emails WHERE id = ?1 AND user_id = ?3 AND verified_at IS NOT NULL)",
777 )
778 .bind(&[row.id.as_str().into(), row.email.as_str().into(), user_id.into()])?
779 .run()
780 .await?;
781 Ok(())
782 }
783
784 /// `update_email_settings`.
785 pub async fn update_email_settings(&self, a: EmailSettingsArgs) -> Result<Outcome<AccountEmails>> {
786 if !is_person(&a.user) {
787 return Ok(Outcome::fail(FailureCode::Forbidden, PEOPLE_ONLY));
788 }
789 let Some(account) = self.account_row(&a.user.id).await? else {
790 return Ok(Outcome::fail(FailureCode::NotFound, "No such account."));
791 };
792 let rows = self.email_rows(&a.user.id).await?;
793 let find = |email: &str| {
794 let email = normalize_email(email).unwrap_or_default();
795 rows.iter().find(|row| row.email == email).cloned()
796 };
797 let primary = match a.primary.as_deref().map(str::trim).filter(|email| !email.is_empty()) {
798 None => None,
799 Some(email) => {
800 let normalized = normalize_email(email).unwrap_or_default();
801 if let Some(why) = primary_refusal(&states(&rows, account.primary_email_id.as_deref()), &normalized) {
802 return Ok(Outcome::fail(FailureCode::Conflict, why));
803 }
804 find(email).filter(|row| Some(row.id.as_str()) != account.primary_email_id.as_deref())
805 }
806 };
807 // Some(None): the primary only.
808 let backup: Option<Option<EmailRow>> = match a.backup.as_deref().map(str::trim) {
809 None => None,
810 Some("") => (account.backup_email_id.is_some()).then_some(None),
811 Some(email) => {
812 let Some(row) = find(email).filter(|row| row.verified_at.is_some()) else {
813 return Ok(Outcome::fail(FailureCode::Conflict, "Only a confirmed address on your account can be the backup."));
814 };
815 let will_be_primary = primary.as_ref().map_or(account.primary_email_id.clone(), |row| Some(row.id.clone()));
816 if Some(&row.id) == will_be_primary.as_ref() {
817 return Ok(Outcome::fail(FailureCode::Conflict, "That is your primary address; choose another for the backup."));
818 }
819 (account.backup_email_id.as_deref() != Some(row.id.as_str())).then_some(Some(row))
820 }
821 };
822 if (primary.is_some() || backup.is_some())
823 && let Some(refusal) = self.proof(&a.user.id, &a.reauth).await?.refusal()
824 {
825 return Ok(refusal);
826 }
827 if let Some(row) = &primary {
828 let old = rows.iter().find(|old| Some(old.id.as_str()) == account.primary_email_id.as_deref());
829 self.set_primary(&a.user.id, row).await?;
830 self.log_security(&a.user.id, "primary_email_changed", Some(&row.display), None).await;
831 // Every confirmed address hears of it, the old primary included.
832 self.tell_addresses(
833 &a.user.id,
834 &a.user.username,
835 &format!("{} is now the primary address", row.display),
836 old.filter(|old| old.verified_at.is_some()).map(|old| old.display.as_str()),
837 )
838 .await;
839 self.announce_email("user.primary_email_changed", &a.user.id, false).await;
840 }
841 if let Some(choice) = &backup {
842 self.db
843 .prepare("UPDATE users SET backup_email_id = ? WHERE id = ?")
844 .bind(&[
845 choice.as_ref().map_or(JsValue::NULL, |row| row.id.as_str().into()),
846 a.user.id.as_str().into(),
847 ])?
848 .run()
849 .await?;
850 let said = choice.as_ref().map_or("primary only".to_owned(), |row| row.display.clone());
851 self.log_security(&a.user.id, "backup_email_changed", Some(&said), None).await;
852 let change = match choice {
853 Some(row) => format!("{} now gets security notices too", row.display),
854 None => "Security notices now go to the primary address only".to_owned(),
855 };
856 self.tell_addresses(&a.user.id, &a.user.username, &change, None).await;
857 }
858 let private = a.private_email.filter(|private| *private != (account.private_email != 0));
859 let block = a.block_private_pushes.filter(|block| *block != (account.block_private_pushes != 0));
860 if private.is_some() || block.is_some() {
861 self.db
862 .prepare(
863 "UPDATE users SET private_email = COALESCE(?, private_email),
864 block_private_pushes = COALESCE(?, block_private_pushes) WHERE id = ?",
865 )
866 .bind(&[
867 private.map_or(JsValue::NULL, |on| (on as u8 as f64).into()),
868 block.map_or(JsValue::NULL, |on| (on as u8 as f64).into()),
869 a.user.id.as_str().into(),
870 ])?
871 .run()
872 .await?;
873 let mut said = Vec::new();
874 if let Some(on) = private {
875 said.push(if on { "address kept private" } else { "address used on web commits" });
876 }
877 if let Some(on) = block {
878 said.push(if on { "pushes that expose it refused" } else { "pushes that expose it allowed" });
879 }
880 self.log_security(&a.user.id, "email_privacy_changed", Some(&said.join("; ")), None).await;
881 }
882 self.emails_view(&a.user.id).await
883 }
884
885 /// Who gets a security notice: every confirmed address when `all`,
886 /// otherwise the primary and the backup.
887 pub async fn notice_recipients(&self, user_id: &str, all: bool) -> Result<Vec<String>> {
888 let Some(account) = self.account_row(user_id).await? else {
889 return Ok(Vec::new());
890 };
891 let rows = sorted(self.email_rows(user_id).await?, account.primary_email_id.as_deref());
892 Ok(rows
893 .into_iter()
894 .filter(|row| row.verified_at.is_some())
895 .filter(|row| {
896 all || Some(row.id.as_str()) == account.primary_email_id.as_deref()
897 || Some(row.id.as_str()) == account.backup_email_id.as_deref()
898 })
899 .map(|row| row.display)
900 .collect())
901 }
902
903 /// Tells every confirmed address of an account, and `also` (an address
904 /// that has just left it), what changed. Best effort.
905 pub async fn tell_addresses(&self, user_id: &str, username: &str, change: &str, also: Option<&str>) {
906 let mut to = self.notice_recipients(user_id, true).await.unwrap_or_default();
907 if let Some(also) = also
908 && !to.iter().any(|known| known.eq_ignore_ascii_case(also))
909 {
910 to.push(also.to_owned());
911 }
912 for address in to {
913 if let Err(error) = email::send_security_notice(&self.env, &address, username, change).await {
914 worker::console_error!("security notice failed: {error}");
915 }
916 }
917 }
918
919 /// Tells the primary and the backup what changed. Best effort.
920 pub async fn tell_primary_and_backup(&self, user_id: &str, username: &str, change: &str) {
921 for address in self.notice_recipients(user_id, false).await.unwrap_or_default() {
922 if let Err(error) = email::send_security_notice(&self.env, &address, username, change).await {
923 worker::console_error!("security notice failed: {error}");
924 }
925 }
926 }
927
928 async fn announce_email(&self, kind: &'static str, user_id: &str, by_staff: bool) {
929 let actor = (!by_staff).then_some(user_id);
930 self.announce(
931 kind,
932 actor,
933 UserEmailChanged {
934 user_id: user_id.to_owned(),
935 by_staff,
936 },
937 )
938 .await;
939 }
940
941 // --- Signing in and resetting by any confirmed address ---
942
943 /// The account a password reset for `email` goes to, the address it is
944 /// sent to and that address's id: a confirmed address, or else the
945 /// unconfirmed address a new account signed up with (following the link
946 /// confirms it).
947 pub async fn reset_target(&self, email: &str) -> Result<Option<ResetTarget>> {
948 let Some(email) = normalize_email(email) else {
949 return Ok(None);
950 };
951 let confirmed = self
952 .db
953 .prepare(
954 "SELECT u.id AS user_id, u.username, e.id AS email_id, e.display FROM user_emails e
955 JOIN users u ON u.id = e.user_id WHERE e.email = ? AND e.verified_at IS NOT NULL",
956 )
957 .bind(&[email.as_str().into()])?
958 .first::<ResetTarget>(None)
959 .await?;
960 if confirmed.is_some() {
961 return Ok(confirmed);
962 }
963 self.db
964 .prepare(
965 "SELECT u.id AS user_id, u.username, e.id AS email_id, e.display FROM user_emails e
966 JOIN users u ON u.primary_email_id = e.id
967 WHERE e.email = ? AND e.verified_at IS NULL ORDER BY u.created_at, u.id LIMIT 1",
968 )
969 .bind(&[email.as_str().into()])?
970 .first::<ResetTarget>(None)
971 .await
972 }
973
974 // --- Commits ---
975
976 /// `email_owners`: whose commits these are, by author address.
977 pub async fn email_owners(&self, a: EmailOwnersArgs) -> Result<HashMap<String, EmailOwner>> {
978 #[derive(Deserialize)]
979 struct Row {
980 email: String,
981 id: String,
982 username: String,
983 avatar: Option<String>,
984 }
985 let mut owners = HashMap::new();
986 let mut plain: Vec<String> = Vec::new();
987 let mut by_name: Vec<(String, Option<String>, String)> = Vec::new();
988 for email in a.emails.iter().take(200) {
989 let Some(email) = normalize_email(email) else { continue };
990 if let Some((suffix, username)) = parse_noreply(&email) {
991 by_name.push((username, Some(suffix), email));
992 } else if let Some(username) = email.strip_suffix("@users.g1t.sh") {
993 // What g1t put on the commits it made before noreply
994 // addresses existed.
995 by_name.push((username.to_owned(), None, email.clone()));
996 } else if !plain.contains(&email) {
997 plain.push(email);
998 }
999 }
1000 if !plain.is_empty() {
1001 let marks = vec!["?"; plain.len()].join(", ");
1002 let bind: Vec<JsValue> = plain.iter().map(|email| email.as_str().into()).collect();
1003 let rows = self
1004 .db
1005 .prepare(format!(
1006 "SELECT e.email, u.id, u.username, u.avatar FROM user_emails e JOIN users u ON u.id = e.user_id
1007 WHERE e.verified_at IS NOT NULL AND e.email IN ({marks})"
1008 ))
1009 .bind(&bind)?
1010 .all()
1011 .await?
1012 .results::<Row>()?;
1013 for row in rows {
1014 owners.insert(row.email, EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
1015 }
1016 }
1017 if !by_name.is_empty() {
1018 let names: Vec<&str> = by_name.iter().map(|(name, _, _)| name.as_str()).collect();
1019 let marks = vec!["?"; names.len()].join(", ");
1020 let bind: Vec<JsValue> = names.iter().map(|name| (*name).into()).collect();
1021 let rows = self
1022 .db
1023 .prepare(format!(
1024 "SELECT username AS email, id, username, avatar FROM users WHERE username IN ({marks})"
1025 ))
1026 .bind(&bind)?
1027 .all()
1028 .await?
1029 .results::<Row>()?;
1030 for (username, suffix, email) in by_name {
1031 if let Some(row) = rows.iter().find(|row| row.username == username)
1032 && suffix.as_deref().is_none_or(|suffix| id_suffix(&row.id) == suffix)
1033 {
1034 owners.insert(
1035 email,
1036 EmailOwner { id: row.id.clone(), username: row.username.clone(), avatar: row.avatar.clone() },
1037 );
1038 }
1039 }
1040 }
1041 Ok(owners)
1042 }
1043
1044 /// `commit_identity`.
1045 pub async fn commit_identity(&self, a: CommitIdentityArgs) -> Result<Option<CommitIdentity>> {
1046 #[derive(Deserialize)]
1047 struct Row {
1048 id: String,
1049 username: String,
1050 display_name: Option<String>,
1051 private_email: u8,
1052 primary: Option<String>,
1053 verified: u8,
1054 }
1055 let row = self
1056 .db
1057 .prepare(
1058 "SELECT u.id, u.username, u.display_name, u.private_email, e.email AS \"primary\",
1059 e.verified_at IS NOT NULL AS verified
1060 FROM users u LEFT JOIN user_emails e ON e.id = u.primary_email_id WHERE u.id = ?",
1061 )
1062 .bind(&[a.user_id.as_str().into()])?
1063 .first::<Row>(None)
1064 .await?;
1065 Ok(row.map(|row| {
1066 let noreply = noreply_address(&row.id, &row.username);
1067 let email = match row.primary {
1068 Some(primary) if row.private_email == 0 && row.verified != 0 => primary,
1069 _ => noreply,
1070 };
1071 let name = row.display_name.filter(|name| !name.trim().is_empty()).unwrap_or(row.username);
1072 CommitIdentity { name, email }
1073 }))
1074 }
1075
1076 /// `push_email_guard`: the addresses a push by this person must not
1077 /// publish, while they keep their address private and block pushes
1078 /// that expose it. One read of the account and one of its addresses.
1079 pub async fn push_email_guard(&self, a: CommitIdentityArgs) -> Result<Option<PushEmailGuard>> {
1080 let Some(account) = self.account_row(&a.user_id).await? else {
1081 return Ok(None);
1082 };
1083 if !guards_pushes(&account) {
1084 return Ok(None);
1085 }
1086 let rows = self.email_rows(&a.user_id).await?;
1087 Ok(Some(PushEmailGuard {
1088 emails: rows.into_iter().filter(|row| row.verified_at.is_some()).map(|row| row.email.to_lowercase()).collect(),
1089 noreply: noreply_address(&account.id, &account.username),
1090 }))
1091 }
1092
1093 // --- Staff ---
1094
1095 /// `admin_user`.
1096 pub async fn admin_user(&self, a: UsernameArgs) -> Result<Option<AdminUser>> {
1097 #[derive(Deserialize)]
1098 struct Id {
1099 id: String,
1100 }
1101 let found = self
1102 .db
1103 .prepare("SELECT id FROM users WHERE username = ?")
1104 .bind(&[a.username.trim().to_lowercase().into()])?
1105 .first::<Id>(None)
1106 .await?;
1107 let Some(found) = found else {
1108 return Ok(None);
1109 };
1110 self.admin_user_by_id(&found.id).await
1111 }
1112
1113 async fn admin_user_by_id(&self, user_id: &str) -> Result<Option<AdminUser>> {
1114 let Some(account) = self.account_row(user_id).await? else {
1115 return Ok(None);
1116 };
1117 let rows = self.email_rows(user_id).await?;
1118 let log = self.security_events(user_id, true).await?;
1119 let emails = view(&account, rows);
1120 Ok(Some(AdminUser {
1121 id: account.id,
1122 username: account.username,
1123 created_at: account.created_at,
1124 emails: emails.emails,
1125 private_email: emails.private_email,
1126 log,
1127 }))
1128 }
1129
1130 /// `admin_remove_email`.
1131 pub async fn admin_remove_email(&self, a: AdminRemoveEmailArgs) -> Result<Outcome<AdminUser>> {
1132 let reason = a.reason.trim();
1133 if reason.is_empty() {
1134 return Ok(Outcome::fail(FailureCode::Invalid, "Say why the address is being removed; the person sees it."));
1135 }
1136 if a.staff.trim().is_empty() {
1137 return Ok(Outcome::fail(FailureCode::Invalid, "Staff changes name who made them."));
1138 }
1139 let Some(user) = self.admin_user(UsernameArgs { username: a.username.clone() }).await? else {
1140 return Ok(Outcome::fail(FailureCode::NotFound, "No such account."));
1141 };
1142 let Some(account) = self.account_row(&user.id).await? else {
1143 return Ok(Outcome::fail(FailureCode::NotFound, "No such account."));
1144 };
1145 let email = normalize_email(&a.email).unwrap_or_default();
1146 let rows = sorted(self.email_rows(&user.id).await?, account.primary_email_id.as_deref());
1147 let Some(row) = rows.iter().find(|row| row.email == email).cloned() else {
1148 return Ok(Outcome::fail(FailureCode::NotFound, "That address is not on this account."));
1149 };
1150 let confirmed: Vec<&EmailRow> = rows.iter().filter(|other| other.verified_at.is_some()).collect();
1151 if row.verified_at.is_some() && confirmed.len() <= 1 {
1152 return Ok(Outcome::fail(
1153 FailureCode::Conflict,
1154 "That is the account's only confirmed address. The person has to add and confirm another first.",
1155 ));
1156 }
1157 let was_primary = account.primary_email_id.as_deref() == Some(row.id.as_str());
1158 if was_primary {
1159 match confirmed.iter().find(|other| other.id != row.id) {
1160 Some(next) => self.set_primary(&user.id, next).await?,
1161 None => {
1162 // An unconfirmed primary on an account with no
1163 // confirmed address: it is left without one.
1164 self.db
1165 .prepare("UPDATE users SET primary_email_id = NULL, email = NULL, email_verified_at = NULL WHERE id = ?")
1166 .bind(&[user.id.as_str().into()])?
1167 .run()
1168 .await?;
1169 }
1170 }
1171 }
1172 self.delete_address(&user.id, &row).await?;
1173 let staff = (a.staff.trim(), reason);
1174 self.log_security(&user.id, "email_removed", Some(&row.display), Some(staff)).await;
1175 let removed = row.verified_at.is_some().then_some(row.display.as_str());
1176 self.tell_addresses(&user.id, &user.username, &format!("g1t staff removed {} ({reason})", row.display), removed)
1177 .await;
1178 self.announce_email("user.email_removed", &user.id, true).await;
1179 if was_primary {
1180 self.announce_email("user.primary_email_changed", &user.id, true).await;
1181 }
1182 Ok(match self.admin_user_by_id(&user.id).await? {
1183 Some(user) => Outcome::Ok(user),
1184 None => Outcome::fail(FailureCode::NotFound, "No such account."),
1185 })
1186 }
1187}
1188
1189/// Whether a code and link that stop working at `expires_at` still work
1190/// at `now` (both RFC 3339, which sort as they read).
1191pub fn still_works(expires_at: &str, now: &str) -> bool {
1192 expires_at > now
1193}
1194
1195/// Which of the account's outstanding codes `code` is, by index: each
1196/// `(token id, code hash)` is compared in constant time, and every one is
1197/// compared whatever matched before it.
1198pub fn matching_code<'a>(key: &[u8], code: &str, sent: impl Iterator<Item = (&'a str, &'a str)>) -> Option<usize> {
1199 let mut found = None;
1200 for (index, (id, hash)) in sent.enumerate() {
1201 let expected = crypto::code_hash(key, id, code);
1202 if crypto::same(&expected, hash) && found.is_none() {
1203 found = Some(index);
1204 }
1205 }
1206 if code.is_empty() { None } else { found }
1207}
1208
1209/// Where a password reset goes.
1210#[derive(Debug, Deserialize)]
1211pub struct ResetTarget {
1212 pub user_id: String,
1213 pub username: String,
1214 pub email_id: String,
1215 pub display: String,
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220 use super::*;
1221
1222 fn row(id: &str, email: &str, verified: bool, created: &str) -> EmailRow {
1223 EmailRow {
1224 id: id.into(),
1225 email: email.into(),
1226 display: email.to_uppercase(),
1227 verified_at: verified.then(|| "2026-10-01T00:00:00.000Z".to_owned()),
1228 sent_at: None,
1229 created_at: created.into(),
1230 }
1231 }
1232
1233 fn account(primary: Option<&str>, backup: Option<&str>, private: bool) -> AccountRow {
1234 AccountRow {
1235 id: "usr_01j9zq4m8x7k2v5n3b6c1d0efg".into(),
1236 username: "ada".into(),
1237 primary_email_id: primary.map(Into::into),
1238 backup_email_id: backup.map(Into::into),
1239 private_email: private as u8,
1240 block_private_pushes: 0,
1241 created_at: String::new(),
1242 }
1243 }
1244
1245 #[test]
1246 fn the_primary_comes_first_then_confirmed_then_the_rest() {
1247 let rows = vec![
1248 row("e1", "old@x.io", false, "2026-01-01"),
1249 row("e2", "work@x.io", true, "2026-02-01"),
1250 row("e3", "home@x.io", true, "2026-03-01"),
1251 ];
1252 let order: Vec<String> = sorted(rows, Some("e3")).into_iter().map(|row| row.id).collect();
1253 assert_eq!(order, ["e3", "e2", "e1"]);
1254 }
1255
1256 #[test]
1257 fn the_view_marks_primary_and_backup_and_shows_addresses_as_typed() {
1258 let rows = vec![row("e1", "a@x.io", true, "1"), row("e2", "b@x.io", true, "2"), row("e3", "c@x.io", false, "3")];
1259 let seen = view(&account(Some("e1"), Some("e2"), true), rows);
1260 assert_eq!(seen.emails[0].email, "A@X.IO");
1261 assert!(seen.emails[0].primary && !seen.emails[0].backup);
1262 assert!(seen.emails[1].backup && !seen.emails[1].primary);
1263 assert!(!seen.emails[2].verified);
1264 assert_eq!(seen.noreply, "6c1d0efg+ada@users.noreply.g1t.sh");
1265 assert_eq!(seen.commit_email, seen.noreply);
1266 assert_eq!(seen.limit, 10);
1267 }
1268
1269 #[test]
1270 fn commits_use_the_primary_only_when_the_person_allows_it_and_it_is_confirmed() {
1271 let confirmed = row("e1", "a@x.io", true, "1");
1272 let unconfirmed = row("e2", "b@x.io", false, "2");
1273 assert_eq!(commit_email(false, Some(&confirmed), "n@noreply"), "a@x.io");
1274 assert_eq!(commit_email(true, Some(&confirmed), "n@noreply"), "n@noreply");
1275 assert_eq!(commit_email(false, Some(&unconfirmed), "n@noreply"), "n@noreply");
1276 assert_eq!(commit_email(false, None, "n@noreply"), "n@noreply");
1277 }
1278
1279 #[test]
1280 fn pushes_are_guarded_only_while_private_and_blocking() {
1281 let mut ada = account(None, None, true);
1282 assert!(!guards_pushes(&ada));
1283 ada.block_private_pushes = 1;
1284 assert!(guards_pushes(&ada));
1285 ada.private_email = 0;
1286 assert!(!guards_pushes(&ada));
1287 }
1288
1289 #[test]
1290 fn a_code_works_for_its_own_link_and_address_only_and_not_after_a_new_email() {
1291 let key = b"identity key".as_slice();
1292 let first = ("link-1", crypto::code_hash(key, "link-1", "482913"));
1293 let other_address = ("link-2", crypto::code_hash(key, "link-2", "100200"));
1294 fn sent<'a>(rows: &'a [(&'static str, String)]) -> Vec<(&'static str, &'a str)> {
1295 rows.iter().map(|(id, hash)| (*id, hash.as_str())).collect()
1296 }
1297 let rows = vec![first.clone(), other_address.clone()];
1298 assert_eq!(matching_code(key, "482913", sent(&rows).into_iter()), Some(0));
1299 assert_eq!(matching_code(key, "100200", sent(&rows).into_iter()), Some(1));
1300 // A wrong code, an empty one, or the right one under another key.
1301 assert_eq!(matching_code(key, "482914", sent(&rows).into_iter()), None);
1302 assert_eq!(matching_code(key, "", sent(&rows).into_iter()), None);
1303 assert_eq!(matching_code(b"another key", "482913", sent(&rows).into_iter()), None);
1304 // Used, or replaced by a new email: the row is gone, and the new
1305 // pair's code is bound to its own link, so the old code fails.
1306 let resent = vec![("link-3", crypto::code_hash(key, "link-3", "731055")), other_address];
1307 assert_eq!(matching_code(key, "482913", sent(&resent).into_iter()), None);
1308 assert_eq!(matching_code(key, "731055", sent(&resent).into_iter()), Some(0));
1309 }
1310
1311 #[test]
1312 fn a_code_and_link_stop_working_after_an_hour() {
1313 let sent = 1_800_000_000_000;
1314 let expires = rfc3339(sent + CONFIRM_TTL_SECONDS * 1000);
1315 assert!(still_works(&expires, &rfc3339(sent)));
1316 assert!(still_works(&expires, &rfc3339(sent + 59 * 60 * 1000)));
1317 assert!(!still_works(&expires, &rfc3339(sent + 60 * 60 * 1000)));
1318 assert!(!still_works(&expires, &rfc3339(sent + 61 * 60 * 1000)));
1319 // Long enough to switch to a mail app, short enough that six
1320 // digits are not worth guessing.
1321 assert!((30 * 60..=60 * 60).contains(&CONFIRM_TTL_SECONDS));
1322 }
1323
1324 #[test]
1325 fn a_link_can_be_sent_again_after_a_minute() {
1326 let now = 1_800_000_000_000;
1327 assert!(may_resend(None, now));
1328 assert!(!may_resend(Some(&rfc3339(now - 30_000)), now));
1329 assert!(may_resend(Some(&rfc3339(now - 61_000)), now));
1330 }
1331}