Make the signin - request access pipeline easier #116

Closed
opened 2026-08-19 02:30:10 +00:00 by Grandiras · 2 comments
Owner

People will try to sign in, then fail because of no access, instead of going directly to the request site (users can be dumb :D). So why not enhance the no access site to directly include the request form with prefilled data, so only the missing pieces need to be filled in and can be filled in with no extra steps. Should make this experience more seamless.

People will try to sign in, then fail because of no access, instead of going directly to the request site (users can be dumb :D). So why not enhance the no access site to directly include the request form with prefilled data, so only the missing pieces need to be filled in and can be filled in with no extra steps. Should make this experience more seamless.
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/291); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-116/run-291.md) — updating every ~5 minutes while the task runs.
Collaborator

Pushed claude/issue-116.

What changed

/access-denied no longer points at /request — it is the request form. By the time anyone reads that page OAuth has just proven who they are, so there is no handle field at all, the name comes prefilled off their Bluesky profile, and asking is one button with email and message left optional.

The access-denied page with the request form embedded: the handle shown as proven rather than asked for, the name prefilled from the profile

Three things follow from the requester being identified rather than anonymous, and each is part of making the flow seamless rather than a side effect:

  • The request carries a verified DID. AccessRequest.Did is normally filled in at approval, when an admin's click resolves a typed handle; posted from here it is set at creation from the cookie, never from a form field. On a pending request a DID can therefore only have come from OAuth — the admin card marks it SIGNED IN, which is the difference between approving an account and approving a claim about one. Approval also stopped depending on handle resolution: RequestsPanel now looks the profile up by request.Did ?? request.Handle, so a handle that has since moved (or whose domain is down) no longer stands between an admin and the decision.
  • There is no secret status link to keep. Signing in again is the status check, so a second visit renders the pending state and the POST redirects back here instead of to /request/{token}.
  • One open request per person. A reload or an impatient second click would otherwise put the same person in the queue twice; a duplicate is now recognisable, so it is refused.

The same page on a later visit: the request is pending, with no form and no link to keep

It is the same POST /request endpoint, not a second one — the honeypot, rate limit, name gate and address gate stay in one place. It branches only on where errors render (/access-denied?error=…) and where the handle comes from; the form still posts the handle in a hidden field, so a session that expires between render and submit degrades into an ordinary anonymous request rather than a rejection. The invite variant deliberately takes no DID: someone asking for an invite code is asking for a second account. The anonymous /request page is untouched and still the path from /login.

New copy is in both locales, Changelog.Unreleased has a line in both, and docs/access-requests.md, docs/auth.md and docs/testing.md carry the reasoning.

Verification

  • dotnet build Ponente.sln clean (one pre-existing warning in Calendar.razor), dotnet test Ponente.sln 295/295 — including the en/de key parity check and DocumentationTests.
  • The CI container had no dotnet and no container runtime, so I installed the .NET 10 SDK and a local Postgres and ran the app in Development against it, then drove requests.spec.js with Playwright: 10 passed, including the new test. The one failure is full circle: request → approve → invite code → signup → board, which mints an invite on the PDS — there is no PDS container in this improvised stack ("The account server is currently unreachable"), and it is untouched by this change.
  • The screenshots above are that running app, signed in as a real non-member Bluesky account so the profile prefill is genuine rather than staged.

The new e2e test needed a visitor who is signed in and not a member, which the Members table cannot produce because it is the allowlist. So /dev/login/{did} gained a ?handle= form that signs in a DID with no member row — still Development-only, still the one backdoor, and explicit rather than the default for a missing row so a typo'd DID keeps 404ing instead of quietly signing a test in as a stranger.

No follow-up issues to file.


⏱ 19m 37s · 110 turns · tokens: 176.4k in / 59.4k out (+13330.4k cache-read)
📜 Full session transcript
→ Opened PR: #121

Pushed `claude/issue-116`. ## What changed `/access-denied` no longer points at `/request` — it **is** the request form. By the time anyone reads that page OAuth has just proven who they are, so there is no handle field at all, the name comes prefilled off their Bluesky profile, and asking is one button with email and message left optional. ![The access-denied page with the request form embedded: the handle shown as proven rather than asked for, the name prefilled from the profile](https://git.grandiras.net/attachments/e3eb04e8-158c-41cc-bed9-89f32c0fbedc) Three things follow from the requester being *identified* rather than anonymous, and each is part of making the flow seamless rather than a side effect: - **The request carries a verified DID.** `AccessRequest.Did` is normally filled in at approval, when an admin's click resolves a typed handle; posted from here it is set at creation from the cookie, never from a form field. On a pending request a DID can therefore only have come from OAuth — the admin card marks it `SIGNED IN`, which is the difference between approving an account and approving a claim about one. Approval also stopped depending on handle resolution: `RequestsPanel` now looks the profile up by `request.Did ?? request.Handle`, so a handle that has since moved (or whose domain is down) no longer stands between an admin and the decision. - **There is no secret status link to keep.** Signing in again *is* the status check, so a second visit renders the pending state and the POST redirects back here instead of to `/request/{token}`. - **One open request per person.** A reload or an impatient second click would otherwise put the same person in the queue twice; a duplicate is now recognisable, so it is refused. ![The same page on a later visit: the request is pending, with no form and no link to keep](https://git.grandiras.net/attachments/a40338d1-ffec-41bc-ab6c-11e119308ccd) It is the **same `POST /request` endpoint**, not a second one — the honeypot, rate limit, name gate and address gate stay in one place. It branches only on where errors render (`/access-denied?error=…`) and where the handle comes from; the form still posts the handle in a hidden field, so a session that expires between render and submit degrades into an ordinary anonymous request rather than a rejection. The invite variant deliberately takes no DID: someone asking for an invite code is asking for a *second* account. The anonymous `/request` page is untouched and still the path from `/login`. New copy is in both locales, `Changelog.Unreleased` has a line in both, and `docs/access-requests.md`, `docs/auth.md` and `docs/testing.md` carry the reasoning. ## Verification - `dotnet build Ponente.sln` clean (one pre-existing warning in `Calendar.razor`), `dotnet test Ponente.sln` **295/295** — including the en/de key parity check and `DocumentationTests`. - The CI container had no dotnet and no container runtime, so I installed the .NET 10 SDK and a local Postgres and ran the app in Development against it, then drove `requests.spec.js` with Playwright: **10 passed**, including the new test. The one failure is `full circle: request → approve → invite code → signup → board`, which mints an invite on the PDS — there is no PDS container in this improvised stack ("The account server is currently unreachable"), and it is untouched by this change. - The screenshots above are that running app, signed in as a real non-member Bluesky account so the profile prefill is genuine rather than staged. The new e2e test needed a visitor who is signed in and *not* a member, which the `Members` table cannot produce because it **is** the allowlist. So `/dev/login/{did}` gained a `?handle=` form that signs in a DID with no member row — still Development-only, still the one backdoor, and explicit rather than the default for a missing row so a typo'd DID keeps 404ing instead of quietly signing a test in as a stranger. No follow-up issues to file. --- ⏱ 19m 37s · 110 turns · tokens: 176.4k in / 59.4k out (+13330.4k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-116/run-291.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/121
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/Ponente#116
No description provided.