Skip to content

feat(registration): anonymous self-signup with email-domain allow-list (#62)#106

Open
martinydeAI wants to merge 11 commits into
developfrom
feature/issue-62-self-signup
Open

feat(registration): anonymous self-signup with email-domain allow-list (#62)#106
martinydeAI wants to merge 11 commits into
developfrom
feature/issue-62-self-signup

Conversation

@martinydeAI

Copy link
Copy Markdown
Collaborator

Links to issues

Closes #62.

do-not-merge — depends on PR #105.

This branch is rebased on top of feature/issue-63-account-status-checker
(PR #105) because the integration tests assert that a freshly
registered Pending user cannot sign in — which requires the
AccountStatusChecker from PR #105. Once PR #105 merges into
develop, this branch is rebased onto develop (dropping the
now-redundant checker commits), the do-not-merge label is
removed, and this PR becomes mergeable on its own.

Description

Replaces PR #90. The original PR #90 was based on PR #88's now-orphaned
branch (PR #88 was "merged" into a stacked base that never reached
develop). This PR rebases the registration work onto PR #105 (the
replacement for PR #88's content) so it can land cleanly on develop
once PR #105 does.

Adds anonymous self-signup at /register:

  • GET /register — renders the form (email, name, password,
    password confirmation, CSRF token).
  • POST /register — validates the inputs, creates the user with
    status = Pending, redirects to /register/pending. On validation
    failure: 422 + re-render with a localised error.
  • GET /register/pending — "thanks, your account is awaiting
    approval" landing page.

The validation logic lives in App\Security\Registration (service);
the controller stays thin per project conventions. Each failure
raises a RegistrationException carrying a translation key, which
the controller renders through |trans. CSRF-protected via
csrf_token('register').

Domain allow-list: comma-separated env var
REGISTRATION_ALLOWED_EMAIL_DOMAINS=aarhus.dk,kk.dk,…, bound via
%env(default:…:REGISTRATION_ALLOWED_EMAIL_DOMAINS)% with
example.test as the dev / test default so the app boots without
the var set. Parsing happens once in
App\Security\AllowedEmailDomains (lowercase + trim + dedup + drop
empties).

A pending user created by this route cannot sign in: the integration
test covers the hand-off through PR #105'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

  • My code is covered by test cases.
  • My code passes our test (all our tests).
  • My code passes our static analysis suite.
  • My code passes our continuous integration process.

Verified locally:

  • task coding-standards-check — green.
  • task test-coverage124 tests, 406 assertions, 100% coverage.

Additional comments or questions

The branch currently carries PR #105's four commits at its base
(rebased from feature/issue-63-account-status-checker). The
"net" diff this PR adds on top is the seven registration-specific
commits. Once PR #105 merges, a rebase onto develop drops the
duplicate base commits and the diff cleans up to just the
registration work.


Details - AI specificities

martinydeAI and others added 8 commits June 22, 2026 14:21
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.
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>
PHP CS Fixer flagged a trailing-space-only line that survived the
rebase. Pure formatting fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@martinydeAI martinydeAI added the do-not-merge Block merging until external dependency lands label Jun 22, 2026
@martinyde martinyde removed the do-not-merge Block merging until external dependency lands label Jun 22, 2026
@martinyde martinyde requested a review from tuj June 22, 2026 13:09
Comment thread config/services.yaml Outdated
Comment thread config/services.yaml Outdated
Comment thread src/Security/AllowedEmailDomains.php Outdated
martinydeAI and others added 2 commits June 23, 2026 11:46
Address PR #106 review feedback:

- Move the `REGISTRATION_ALLOWED_EMAIL_DOMAINS` env binding from
  the services.yaml parameter + bind block to an `#[Autowire]`
  attribute on `AllowedEmailDomains::__construct`. The wiring
  now lives next to the consumer instead of in central config.
- Default the var to empty in `.env` (with a comment explaining
  how to populate it) so production must opt in to allowing
  signup, instead of silently accepting `example.test`.
- Set `REGISTRATION_ALLOWED_EMAIL_DOMAINS=example.test` in
  `.env.test` so the existing registration tests keep the same
  baseline without leaking the placeholder into other envs.
- Drop the redundant `parameters` entry from `services.yaml`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: anonymous self-signup with email-domain allow-list

3 participants