feat(registration): anonymous self-signup with email-domain allow-list (#62)#90
Open
martinydeAI wants to merge 7 commits into
Open
Conversation
Adds the public `/register` route per ADR 006: - `App\Controller\RegistrationController` — thin: validates CSRF, delegates to a service, renders the form / pending page. GET + POST on `/register`; GET on `/register/pending`. Both routes are added to `access_control` as `PUBLIC_ACCESS`. Already-authenticated visitors are bounced to `/`. - `App\Security\Registration` — orchestrates email validation, the domain allow-list check, password-match check, name + password non-emptiness, and the persistence step via the existing `UserManager`. Persists with `status = Pending`. Each failure raises a `RegistrationException` carrying a translation key. - `App\Security\AllowedEmailDomains` — value object that wraps the comma-separated env var `REGISTRATION_ALLOWED_EMAIL_DOMAINS`, normalising on construction (lowercase, trim, dedup, drop empties). Bound to services via `%env(default:…:REGISTRATION_ALLOWED_EMAIL_DOMAINS)%` with `example.test` as the default so dev / tests boot without needing the var set. - Twig templates re-use the existing `Form/*`, `Eyebrow`, and `Layout/*` components; no new component families. - Translations live in the existing `messages` domain under `register.*`. A pending user created by this route cannot sign in (covered by the checker from PR #88) — the integration test exercises that hand-off end-to-end. Sequenced as PR 4 of the User management milestone plan. Stacked on PR 3 (#88) since `UserManager::createUser(..., status: Pending)` is only useful when the checker rejects pending logins; PR 3 is itself stacked on PR 1 (#86) for the `name` + `status` fields. Labelled `do-not-merge` until both bases land. The domain-extraction helper from PR #87 (`App\Security\EmailDomain`) isn't pulled in here to keep the stack chain clean — the same one-line extraction is inlined in `Registration` with a TODO to fold back into the helper once PR #87 lands on `develop`. Closes #62. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Correct comment formatting for allowed email domains.
Removed inlined comments regarding EmailDomain dependency and clarified the self-signup rules.
martinyde
requested changes
Jun 19, 2026
| } | ||
|
|
||
| #[Route(path: '/register', name: 'app_register', methods: ['GET', 'POST'])] | ||
| public function register(Request $request): Response |
Contributor
There was a problem hiding this comment.
Does this need a helper class to keep controller thin as per CLAUD.md directions
Per review on PR #90 - apply the test-comment convention to the three new test files (RegistrationControllerTest, AllowedEmailDomainsTest, RegistrationTest). Each `test...` method now opens with a single-line "Tests ...", "Ensures ...", or "Verifies ..." comment naming what it asserts. Pure documentation change - no test logic touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Links to issues
Closes #62.
Description
Anonymous self-signup at
/registerGET /register— render the form (email, name, password, passwordconfirmation, CSRF token).
POST /register— validate input, create the user withstatus = Pending, redirect to/register/pending. On validationfailure: 422 + re-render with a localised error.
GET /register/pending— "thanks, your account is awaitingapproval" page.
The validation logic lives in
App\Security\Registration(service);the controller stays thin per project conventions. Each failure
raises a
RegistrationExceptioncarrying a translation key, whichthe controller renders through
|trans. CSRF-protected via Symfony'scsrf_token('register')helper.Allow-list: comma-separated env var
REGISTRATION_ALLOWED_EMAIL_DOMAINS=aarhus.dk,kk.dk,…Bound via
%env(default:…:REGISTRATION_ALLOWED_EMAIL_DOMAINS)%withexample.testas the dev / test default so the app boots withoutthe var set. Parsing happens once in
App\Security\AllowedEmailDomains(lowercase + trim + dedup + dropempties).
A pending user created by this route cannot sign in: the
integration test covers the hand-off through PR #88's
AccountStatusChecker.Screenshot of the result
n/a — form re-uses the same
Form/*components and base layout as/login. Manual screenshot can be added on request.Checklist
Additional comments or questions
The plan also lists a
Domainentity follow-up: once PR #80(Organization entity) lands,
User.organizationis added (separateissue), and the registration allow-list switches from this env-var
to
Organization.emailDomains. That swap is not in scope forthis PR.
UserManager::createUser()is reused as the persistence step — noduplication of the password-hashing / unique-email logic.
The domain-extraction helper from PR #87 (
App\Security\EmailDomain)isn't pulled in here to keep the stack chain clean — the same one-
line extraction is inlined in
Registrationwith a comment pointingat PR #87 so it can be folded back once both PRs land.
Details - AI specificities
docs/adr/006-user-approval-and-account-state.md(Draft; lands via docs: add ADR 004 — user registration, approval, and account state #61). Specifically the "Registration" section.
tests/Integration/Controller/RegistrationControllerTest.php(functional end-to-end)
tests/Unit/Security/RegistrationTest.php(service unit)tests/Unit/Security/AllowedEmailDomainsTest.php(value-objectunit)
(per ADR 006).
matrix with mocked collaborators (fast). The functional test
exercises the route end-to-end against the real firewall, the
real password hasher (cheap cost in test), and the
AccountStatusCheckerfrom PR feat(security): reject pending and blocked users at login (#63) #88 so the "register → can't log inyet" hand-off is genuinely covered.