diff --git a/server/src/at_record_server/handlers/oauth.gleam b/server/src/at_record_server/handlers/oauth.gleam index 741af94..649dd93 100644 --- a/server/src/at_record_server/handlers/oauth.gleam +++ b/server/src/at_record_server/handlers/oauth.gleam @@ -2,6 +2,7 @@ //// logout. The hard parts (PAR/PKCE/DPoP/token exchange) live under oauth/; //// these are just the HTTP-facing glue. +import at_record_server/browse import at_record_server/context.{type Context, error_json} import at_record_server/known_users import at_record_server/oauth/flow @@ -9,6 +10,7 @@ import at_record_server/oauth/session_store import at_record_server/oauth/sessions import at_record_server/oauth/store import at_record_server/oauth/tokens +import gleam/int import gleam/json import gleam/list import gleam/option.{None, Some} @@ -167,6 +169,7 @@ fn exchange_and_start_session( handle: oauth_session.handle, pds: oauth_session.pds, )) + backfill_user_catalog(ctx, oauth_session) wisp.log_info("oauth session created for " <> tok.sub) wisp.redirect("/") |> session_store.set(req, id) @@ -176,3 +179,27 @@ fn exchange_and_start_session( } } } + +/// Seeds this one user's existing releases into `catalog_index` on login: the +/// one-time boot backfill (`backfill_catalog_index` in `at_record_server`) +/// only ever covers users already known at boot, so a first-time login is the +/// only chance to backfill a user's pre-existing repo contents (Jetstream only +/// surfaces commits from the point it started tailing, not history). Reuses +/// `browse.fetch_user_releases`, which is already best-effort (empty list on +/// any fetch/decode failure), so this can never fail the login. +fn backfill_user_catalog(ctx: Context, session: sessions.OauthSession) -> Nil { + let user = + known_users.KnownUser( + did: session.did, + handle: session.handle, + pds: session.pds, + ) + let rows = browse.fetch_user_releases(ctx.atproto.client, user) + rows |> list.each(ctx.catalog_index.upsert) + wisp.log_info( + "catalog_index: backfilled " + <> int.to_string(list.length(rows)) + <> " releases for " + <> session.did, + ) +} diff --git a/server/test/oauth_test.gleam b/server/test/oauth_test.gleam index 9cdf804..34767fc 100644 --- a/server/test/oauth_test.gleam +++ b/server/test/oauth_test.gleam @@ -437,3 +437,55 @@ pub fn callback_token_exchange_failure_redirects_test() { assert response.get_header(resp, "location") == Ok("/?error=token_exchange_failed") } + +const list_records_response = "{\"records\":[{\"uri\":\"at://did:plc:abc/dev.mokkenstorm.crate.catalog.release/r1\",\"cid\":\"bafycid\",\"value\":{\"createdAt\":\"2024-01-01T00:00:00Z\",\"title\":\"Loveless\"}}]}" + +/// A successful login must backfill the just-logged-in user's own existing +/// releases into `catalog_index`: that repo's contents predate this login and +/// Jetstream only tails commits from here forward, so this is the only chance +/// to seed them. +pub fn callback_success_backfills_new_users_releases_into_catalog_index_test() { + let stub = + xrpc.Client(send: fn(req) { + case string.contains(req.path, "listRecords") { + True -> + Ok(response.Response( + 200, + [], + bit_array.from_string(list_records_response), + )) + False -> + Ok(response.Response(200, [], bit_array.from_string(token_response))) + } + }) + let cfg = test_cfg(stub) + let assert Ok(index) = catalog_index.start() + let ctx = Context(..test_context(cfg), catalog_index: index) + let state = "test-state-success" + let issuer = "https://as.example" + store.put( + cfg.store, + state, + store.PendingFlow( + handle: "h.test", + pds: "https://pds.example", + issuer:, + token_endpoint: issuer <> "/token", + dpop_key: gose.generate_ec(ec.P256), + pkce_verifier: "verifier", + client_id: cfg.client_id, + confidential: False, + ), + ) + let req = + request_with_state_cookie( + "/api/oauth/callback?code=abc&state=" <> state <> "&iss=" <> issuer, + state, + ) + let resp = oauth_handler.callback(req, ctx) + assert resp.status == 303 + + let assert [row] = index.list() + assert row.title == "Loveless" + assert row.publisher_did == "did:plc:abc" +}