Release 0.8.0: open beta-access request flow (first/last/why)

Replaces the v0.3.0 / v0.7.0 allowed_emails admission gate with an
admin-grant flow (roadmap item #6, SPEC §6.1 / §6.2 / §14.1 / §17).
Any valid email can sign in via OTC; a fresh user lands in
permission_state='pending' with a captured first/last/why profile,
and an admin grant flips them to 'granted' before write endpoints
accept them. Grandfathered users pass through the migration with
the column default 'granted' so existing contributors are unaffected.
The allowed_emails table stays in the schema as a fast-path bypass
pending v0.9.0's admin user-management page (item #7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-28 02:25:59 -07:00
parent 8aa65014b4
commit ca8ba69acb
17 changed files with 1327 additions and 145 deletions
+40 -19
View File
@@ -1,38 +1,59 @@
// BetaPending.jsx — the post-OAuth-rejection page.
// BetaPending.jsx — the "your request is in review" page (§6.1 / §14.1).
//
// When a deployment is in private-beta mode (i.e. its `allowed_emails`
// table has any rows), the OAuth callback redirects unrecognised users
// here instead of provisioning them. The framework cannot know the
// deployment operator's preferred contact channel — so the deployment
// supplies one via VITE_BETA_CONTACT (an email, URL, or short
// instruction). If unset, we render a generic ask-the-operator line.
// v0.3.0 introduced this surface as the post-OAuth-rejection page (a
// user whose email wasn't on the `allowed_emails` table bounced here).
// v0.8.0 (roadmap item #6) repurposes it as the post-OTC pending-grant
// page: any authenticated user whose `permission_state='pending'` lands
// here on root visits, after a fresh-OTC profile capture, or via the
// header "Your beta access is in review" affordance.
//
// The deployment supplies a contact channel via VITE_BETA_CONTACT (an
// email, URL, or short instruction). If unset, we render a generic
// ask-the-operator line.
import { Link } from 'react-router-dom'
export default function BetaPending() {
export default function BetaPending({ viewer }) {
const contact = import.meta.env.VITE_BETA_CONTACT || ''
const isPending = viewer?.permission_state === 'pending'
return (
<div className="beta-pending">
<div className="beta-pending-inner">
<h1>{import.meta.env.VITE_APP_NAME} is in private Beta.</h1>
<p>
Discussion and contribution are gated to invited emails for now.
Reading is open every super-draft, every active RFC, and every
public conversation is visible without signing in.
</p>
<h1>
{isPending
? 'Your request is in review.'
: `${import.meta.env.VITE_APP_NAME} is in private Beta.`}
</h1>
{isPending ? (
<>
<p>
Thanks for telling us a bit about yourself. An admin will
review your request and get back to you as soon as we can.
</p>
<p>
While you wait, the catalog on the left lists every super-draft
and active RFC in the framework reading is open. Discussion
and contribution unlock once your access is granted.
</p>
</>
) : (
<p>
Discussion and contribution are gated to invited contributors for
now. Reading is open every super-draft, every active RFC, and
every public conversation is visible without signing in.
</p>
)}
{contact ? (
<p className="beta-pending-contact">
To request access, contact <strong>{contact}</strong> with the
email address you'd like to sign in with.
Questions? Contact <strong>{contact}</strong>.
</p>
) : (
<p className="beta-pending-contact">
To request access, contact the deployment operator with the email
address you'd like to sign in with.
Questions? Contact the deployment operator.
</p>
)}
<div className="beta-pending-actions">
<Link className="btn-primary" to="/">Browse as a guest</Link>
<Link className="btn-primary" to="/">Browse the catalog</Link>
<Link className="btn-link-quiet" to="/philosophy">Read the philosophy </Link>
</div>
</div>