From 86f7ffbe94633a1544fe23107b86160a077ce440 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Wed, 1 Apr 2026 21:39:15 +0000 Subject: [PATCH] Gate registration to ALLOWED_REGISTRATION_EMAIL --- .beads/issues.jsonl | 2 +- app/config/runtime.exs | 2 + app/config/test.exs | 2 + .../user_registration_controller.ex | 42 ++++++++++++------- .../user_registration_controller_test.exs | 30 +++++++++++++ 5 files changed, 62 insertions(+), 16 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 62b0ee8..b372ebf 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -7,6 +7,6 @@ {"id":"firehose-ai8","title":"Add unfiltered post access for dashboard","description":"## Context\nDashboard needs access to all posts including drafts and future-dated.\nAdd unfiltered_posts/0 to Blog macro and all_posts_unfiltered/0 to Registry.\n\n## Scope\n- blogex/lib/blogex/blog.ex: add unfiltered_posts/0\n- blogex/lib/blogex/registry.ex: add all_posts_unfiltered/0\n- blogex/test/support/fake_blog.ex: add unfiltered_posts/0\n- blogex/test/blogex/registry_test.exs: new tests\n\n## TDD\nRED: Test unfiltered returns all posts including drafts and future-dated\nGREEN: Implement functions\nREFACTOR: None","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:44.63593107Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:31:20.37549839Z","closed_at":"2026-04-01T20:31:20.37549839Z","close_reason":"Closed"} {"id":"firehose-apw","title":"Add integration tests for scheduled post filtering in Phoenix","description":"## Context\nPhoenix blog controller tests need to verify date filtering works end-to-end.\nMay need a far-future markdown test fixture (2099/01-01-future-post.md).\n\n## Scope\n- app/test/firehose_web/controllers/blog_test.exs\n- app/priv/blog/engineering/2099/01-01-future-post.md (test fixture)\n\n## TDD\nRED: Blog index hides future post, show page returns it, tag page excludes it\nGREEN: Should pass from Blogex changes\nREFACTOR: None","status":"in_progress","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:16.294363414Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T21:35:39.95804435Z","dependencies":[{"issue_id":"firehose-apw","depends_on_id":"firehose-2wc","type":"blocks","created_at":"2026-04-01T20:07:52.797645635Z","created_by":"Willem van den Ende"},{"issue_id":"firehose-apw","depends_on_id":"firehose-1x3","type":"blocks","created_at":"2026-04-01T20:07:52.829112074Z","created_by":"Willem van den Ende"}]} {"id":"firehose-dhh","title":"Run mix phx.gen.auth and configure","description":"## Context\nNo auth exists. Run mix phx.gen.auth Accounts User users.\nRemove auth links from public nav (login/registration are hidden URLs).\n\n## Scope\n- Generated files in app/lib/firehose/accounts/, app/lib/firehose_web/\n- app/lib/firehose_web/router.ex\n- Layout files (root.html.heex, app.html.heex) — remove injected auth links\n\n## TDD\nRED: Generated tests should pass\nGREEN: Run generator, migrate, verify\nREFACTOR: Remove auth links from public navigation","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:28.010843844Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:31:20.37861782Z","closed_at":"2026-04-01T20:31:20.37861782Z","close_reason":"Closed"} -{"id":"firehose-pp3","title":"Seed demo user in dev","description":"## Context\nSeed demo@example.com / password123 in dev environment only.\nUse Accounts context from phx.gen.auth.\n\n## Scope\n- app/priv/repo/seeds.exs\n\n## TDD\nTrivial — manual verification","status":"in_progress","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:28.091149857Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:32:40.595001752Z","dependencies":[{"issue_id":"firehose-pp3","depends_on_id":"firehose-dhh","type":"blocks","created_at":"2026-04-01T20:08:01.537294098Z","created_by":"Willem van den Ende"}]} +{"id":"firehose-pp3","title":"Seed demo user in dev","description":"## Context\nSeed demo@example.com / password123 in dev environment only.\nUse Accounts context from phx.gen.auth.\n\n## Scope\n- app/priv/repo/seeds.exs\n\n## TDD\nTrivial — manual verification","status":"closed","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:28.091149857Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T21:37:09.561290121Z","closed_at":"2026-04-01T21:37:09.561290121Z","close_reason":"Closed","dependencies":[{"issue_id":"firehose-pp3","depends_on_id":"firehose-dhh","type":"blocks","created_at":"2026-04-01T20:08:01.537294098Z","created_by":"Willem van den Ende"}]} {"id":"firehose-ra3","title":"Show draft/scheduled status banners for authenticated users","description":"## Context\nWhen authenticated user views a draft or scheduled post via direct URL,\nshow a banner: \"Draft — not published\" or \"This post is scheduled for {date}\".\nUnauthenticated users see no banner.\n\n## Scope\n- app/lib/firehose_web/controllers/blog_controller.ex: pass visibility to template\n- app/lib/firehose_web/controllers/blog_html/show.html.heex: conditional banner\n- app/test/firehose_web/controllers/blog_test.exs: banner tests\n\n## TDD\nRED: Auth user sees banner on draft/scheduled, no banner on live, unauth sees no banner\nGREEN: Compute visibility, pass to template, render conditionally\nREFACTOR: Extract banner component if reusable","status":"in_progress","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:44.713739919Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T20:32:40.675871251Z","dependencies":[{"issue_id":"firehose-ra3","depends_on_id":"firehose-4nq","type":"blocks","created_at":"2026-04-01T20:08:01.660225195Z","created_by":"Willem van den Ende"},{"issue_id":"firehose-ra3","depends_on_id":"firehose-dhh","type":"blocks","created_at":"2026-04-01T20:08:01.696919105Z","created_by":"Willem van den Ende"}]} {"id":"firehose-vyw","title":"Verify router respects date filtering","description":"## Context\nBlogex.Router index, tag, and feed routes use all_posts()/posts_by_tag() (now filtered).\nThe /:slug route uses get_post() (now unfiltered). Add tests confirming correct behaviour.\n\n## Scope\n- blogex/test/blogex/router_test.exs\n\n## TDD\nRED: Test GET / excludes future posts, GET /tag/:tag excludes, GET /:slug returns future post\nGREEN: Should pass from Steps 1-2\nREFACTOR: None","status":"in_progress","priority":2,"issue_type":"task","owner":"willem+gitea@livingsoftware.co.uk","created_at":"2026-04-01T20:07:16.253169962Z","created_by":"Willem van den Ende","updated_at":"2026-04-01T21:35:39.918341344Z","dependencies":[{"issue_id":"firehose-vyw","depends_on_id":"firehose-2wc","type":"blocks","created_at":"2026-04-01T20:07:52.73739353Z","created_by":"Willem van den Ende"},{"issue_id":"firehose-vyw","depends_on_id":"firehose-1x3","type":"blocks","created_at":"2026-04-01T20:07:52.770379034Z","created_by":"Willem van den Ende"}]} diff --git a/app/config/runtime.exs b/app/config/runtime.exs index f0f4e40..162e9cb 100644 --- a/app/config/runtime.exs +++ b/app/config/runtime.exs @@ -20,6 +20,8 @@ if System.get_env("PHX_SERVER") do config :firehose, FirehoseWeb.Endpoint, server: true end +config :firehose, :allowed_registration_email, System.get_env("ALLOWED_REGISTRATION_EMAIL") + if config_env() == :prod do database_url = System.get_env("DATABASE_URL") || diff --git a/app/config/test.exs b/app/config/test.exs index 148c651..524ff6b 100644 --- a/app/config/test.exs +++ b/app/config/test.exs @@ -38,3 +38,5 @@ config :phoenix, :plug_init_mode, :runtime # Enable helpful, but potentially expensive runtime checks config :phoenix_live_view, enable_expensive_runtime_checks: true + +config :firehose, :allowed_registration_email, nil diff --git a/app/lib/firehose_web/controllers/user_registration_controller.ex b/app/lib/firehose_web/controllers/user_registration_controller.ex index 3c1ed89..3c4245e 100644 --- a/app/lib/firehose_web/controllers/user_registration_controller.ex +++ b/app/lib/firehose_web/controllers/user_registration_controller.ex @@ -10,23 +10,35 @@ defmodule FirehoseWeb.UserRegistrationController do end def create(conn, %{"user" => user_params}) do - case Accounts.register_user(user_params) do - {:ok, user} -> - {:ok, _} = - Accounts.deliver_login_instructions( - user, - &url(~p"/users/log-in/#{&1}") + allowed_email = Application.get_env(:firehose, :allowed_registration_email) + + if allowed_email == nil or user_params["email"] != allowed_email do + changeset = + %User{} + |> Accounts.change_user_email(user_params) + |> Ecto.Changeset.add_error(:email, "registration is invite only.") + |> Map.put(:action, :validate) + + render(conn, :new, changeset: changeset) + else + case Accounts.register_user(user_params) do + {:ok, user} -> + {:ok, _} = + Accounts.deliver_login_instructions( + user, + &url(~p"/users/log-in/#{&1}") + ) + + conn + |> put_flash( + :info, + "An email was sent to #{user.email}, please access it to confirm your account." ) + |> redirect(to: ~p"/users/log-in") - conn - |> put_flash( - :info, - "An email was sent to #{user.email}, please access it to confirm your account." - ) - |> redirect(to: ~p"/users/log-in") - - {:error, %Ecto.Changeset{} = changeset} -> - render(conn, :new, changeset: changeset) + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :new, changeset: changeset) + end end end end diff --git a/app/test/firehose_web/controllers/user_registration_controller_test.exs b/app/test/firehose_web/controllers/user_registration_controller_test.exs index edd2601..b26a34c 100644 --- a/app/test/firehose_web/controllers/user_registration_controller_test.exs +++ b/app/test/firehose_web/controllers/user_registration_controller_test.exs @@ -23,6 +23,8 @@ defmodule FirehoseWeb.UserRegistrationControllerTest do @tag :capture_log test "creates account but does not log in", %{conn: conn} do email = unique_user_email() + Application.put_env(:firehose, :allowed_registration_email, email) + on_exit(fn -> Application.delete_env(:firehose, :allowed_registration_email) end) conn = post(conn, ~p"/users/register", %{ @@ -37,6 +39,9 @@ defmodule FirehoseWeb.UserRegistrationControllerTest do end test "render errors for invalid data", %{conn: conn} do + Application.put_env(:firehose, :allowed_registration_email, "with spaces") + on_exit(fn -> Application.delete_env(:firehose, :allowed_registration_email) end) + conn = post(conn, ~p"/users/register", %{ "user" => %{"email" => "with spaces"} @@ -47,4 +52,29 @@ defmodule FirehoseWeb.UserRegistrationControllerTest do assert response =~ "must have the @ sign and no spaces" end end + + describe "POST /users/register with email gating" do + test "succeeds when email matches ALLOWED_REGISTRATION_EMAIL", %{conn: conn} do + Application.put_env(:firehose, :allowed_registration_email, "allowed@example.com") + on_exit(fn -> Application.delete_env(:firehose, :allowed_registration_email) end) + + conn = post(conn, ~p"/users/register", %{"user" => %{"email" => "allowed@example.com"}}) + assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "email was sent" + end + + test "fails with invite-only message when email doesn't match", %{conn: conn} do + Application.put_env(:firehose, :allowed_registration_email, "allowed@example.com") + on_exit(fn -> Application.delete_env(:firehose, :allowed_registration_email) end) + + conn = post(conn, ~p"/users/register", %{"user" => %{"email" => "other@example.com"}}) + assert html_response(conn, 200) =~ "registration is invite only" + end + + test "fails with invite-only message when env var is unset", %{conn: conn} do + Application.delete_env(:firehose, :allowed_registration_email) + + conn = post(conn, ~p"/users/register", %{"user" => %{"email" => "anyone@example.com"}}) + assert html_response(conn, 200) =~ "registration is invite only" + end + end end