Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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