0
2
serve: A login names its account by handle at once
Only what this pull adds. It is built on grain runs on a Rust server modelled on knotless, without hatk.
crates/grain-serve/src/index/mod.rs+8−3
| @@ -445,10 +445,15 @@ impl Index { | ||
self.tracked.lock().unwrap().contains(did) | ||
} | ||
| ||
/// Keeps `handle` as the tracked account `did`'s. A handle names one | ||
/// account, so another that had it has none until it is told its own. | ||
pub fn set_handle(&self, did: &str, handle: &str) -> rusqlite::Result<()> { | ||
let db = self.db.lock().unwrap(); | ||
db.execute("UPDATE repo SET handle = ?2 WHERE did = ?1", [did, handle])?; | ||
Ok(()) | ||
let mut db = self.db.lock().unwrap(); | ||
let tx = db.transaction()?; | ||
let sql = "UPDATE repo SET handle = NULL WHERE handle = ?2 AND did <> ?1"; | ||
tx.execute(sql, [did, handle])?; | ||
tx.execute("UPDATE repo SET handle = ?2 WHERE did = ?1", [did, handle])?; | ||
tx.commit() | ||
} | ||
| ||
/// The tracked account `did`'s handle, and how far backfill has it. | ||
crates/grain-serve/src/login.rs+16−0
| @@ -531,9 +531,25 @@ async fn keep( | ||
// Asked again, for an account made or named only now. | ||
anyhow::ensure!(!index(app).taken_down(&session.did)?, "taken down here"); | ||
index(app).keep_web_session(&session, keys::now_micros())?; | ||
named(app, &ctx, &session.did, &session.handle).await?; | ||
Ok(session.did) | ||
} | ||
| ||
/// Tracks `did`, which has logged in, and keeps `handle`, the one its PDS | ||
/// says it has, as its handle if it resolves to `did`: so its profile is | ||
/// found by handle before backfill reads it. A handle that does not is | ||
/// left for backfill to look up. An identity event or backfill later | ||
/// keeps what it finds over it. | ||
async fn named(app: &App, ctx: &Ctx, did: &str, handle: &str) -> anyhow::Result<()> { | ||
index(app).track(did)?; | ||
let handle = handle.to_lowercase(); | ||
let found = identity::resolve_handle(ctx, &handle).await.ok(); | ||
if found.as_deref() == Some(did) { | ||
index(app).set_handle(did, &handle)?; | ||
} | ||
Ok(()) | ||
} | ||
| ||
/// Keeps the session a server hands this client for an account it made, | ||
/// asked for at `url` with `body` and `authorization` | ||
/// ([`oauth::provisioned`]), named by `handle`. Returns the account. Its | ||
crates/grain-serve/tests/login.rs+45−0
| @@ -805,3 +805,48 @@ async fn a_login_seeds_a_grain_profile_from_bluesky() { | ||
.await | ||
.unwrap(); | ||
} | ||
| ||
#[tokio::test] | ||
async fn a_login_names_its_account_by_handle_at_once() { | ||
let site = site().await; | ||
let a = net::account().await; | ||
// An account the index had under a's handle has it no longer once a | ||
// logs in: a handle names one account. | ||
let stale = "did:plc:stalehandleholder"; | ||
site.index.track(stale).unwrap(); | ||
site.index.set_handle(stale, &a.handle).unwrap(); | ||
| ||
let mut jar = Jar::default(); | ||
site.log_in(&mut jar, &a).await; | ||
// Before backfill reads the account. | ||
assert!(site.index.tracks(&a.did)); | ||
let (handle, state) = site.index.repo(&a.did).unwrap().unwrap(); | ||
assert_eq!(handle.as_deref(), Some(a.handle.as_str())); | ||
assert_eq!(state, "pending", "backfill has not read it"); | ||
assert_eq!(site.index.repo(stale).unwrap().unwrap().0, None); | ||
| ||
// Its profile is found by handle once seeded, as by DID. | ||
indexed_or_bare(&site, &a.did).await; | ||
let by_handle = format!("/xrpc/social.grain.actor.getProfile?actor={}", a.handle); | ||
let res = site.get(&mut Jar::default(), &by_handle).await; | ||
assert_eq!(res.status().as_u16(), 200); | ||
let body: Value = res.json().await.unwrap(); | ||
assert_eq!(body["did"], a.did.as_str()); | ||
assert_eq!(body["handle"], a.handle.as_str()); | ||
} | ||
| ||
/// Waits for `did`'s grain profile, named or bare, to be in the index. | ||
async fn indexed_or_bare(site: &web::Site, did: &str) { | ||
let uri = format!("at://{did}/social.grain.actor.profile/self"); | ||
for _ in 0..200 { | ||
let held = site | ||
.index | ||
.get::<grain_serve::index::records::Profile>(&uri) | ||
.unwrap(); | ||
if held.is_some() { | ||
return; | ||
} | ||
tokio::time::sleep(std::time::Duration::from_millis(50)).await; | ||
} | ||
panic!("no profile was seeded for {did}"); | ||
} | ||