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