Skip to content

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

Closed
martinydeAI wants to merge 8 commits into
feature/issue-63-account-status-checkerfrom
feature/issue-62-self-signup
Closed

feat(registration): anonymous self-signup with email-domain allow-list (#62)#90
martinydeAI wants to merge 8 commits into
feature/issue-63-account-status-checkerfrom
feature/issue-62-self-signup

Conversation

@martinydeAI

@martinydeAI martinydeAI commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Links to issues

Closes #62.

do-not-merge — depends on PR #88 (#63) which depends on PR #86
(#45 + #83).

This branch is stacked on feature/issue-63-account-status-checker
because:

  1. It creates users with status = Pending (needs the column from
    PR feat(user): add name and status fields per ADR 006 (#45, #83) #86).
  2. Without PR feat(security): reject pending and blocked users at login (#63) #88's UserCheckerInterface, a freshly-registered
    user could sign in before approval — which defeats the point.

Rebase order once those PRs land:

  1. PR feat(user): add name and status fields per ADR 006 (#45, #83) #86 lands → PR feat(security): reject pending and blocked users at login (#63) #88 rebases onto develop.
  2. PR feat(security): reject pending and blocked users at login (#63) #88 lands → this PR rebases onto develop, do-not-merge
    label removed.

Description

Anonymous self-signup at /register

  • GET /register — render the form (email, name, password, password
    confirmation, CSRF token).
  • POST /register — validate input, create the user with
    status = Pending, redirect to /register/pending. On validation
    failure: 422 + re-render with a localised error.
  • GET /register/pending — "thanks, your account is awaiting
    approval" 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 Symfony's
csrf_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)% 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 #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

  • 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.

Additional comments or questions

The plan also lists a Domain entity follow-up: once PR #80
(Organization entity) lands, User.organization is added (separate
issue), and the registration allow-list switches from this env-var
to Organization.emailDomains. That swap is not in scope for
this PR.

UserManager::createUser() is reused as the persistence step — no
duplication 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 Registration with a comment pointing
at PR #87 so it can be folded back once both PRs land.


Details - AI specificities

@martinydeAI martinydeAI added the do-not-merge Block merging until external dependency lands label Jun 19, 2026
}

#[Route(path: '/register', name: 'app_register', methods: ['GET', 'POST'])]
public function register(Request $request): Response

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need a helper class to keep controller thin as per CLAUD.md directions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread templates/registration/register.html.twig
Comment thread tests/Integration/Controller/RegistrationControllerTest.php
Comment thread tests/Integration/Controller/RegistrationControllerTest.php
Comment thread tests/Unit/Security/AllowedEmailDomainsTest.php
Comment thread tests/Unit/Security/RegistrationTest.php
martinydeAI added a commit that referenced this pull request Jun 19, 2026
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>
@martinyde martinyde requested a review from tuj June 19, 2026 11:47

@tuj tuj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need an extra step where the user email is validated, to make sure the user actually owns the email it registers with.

@martinyde

Copy link
Copy Markdown
Contributor

We need an extra step where the user email is validated, to make sure the user actually owns the email it registers with.

That is addressed in #103 , and future PRs holding email and email confirmation step form

@martinydeAI martinydeAI force-pushed the feature/issue-63-account-status-checker branch from 71ced36 to 7497e88 Compare June 22, 2026 11:54
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

Copy link
Copy Markdown
Collaborator Author

Superseded by #106 — rebased onto PR #105 (the replacement for PR #88), targeting develop directly. This PR's branch was based on PR #88's stacked branch which got orphaned when its content never reached develop. The registration code itself is preserved in #106.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge Block merging until external dependency lands

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants