add backend for join form #308 #438

Open
simon wants to merge 5 commits from feature/concept-web-form-308 into main
Owner

Description of the implemented changes

The changes were:

  • Bugfixing
  • New Feature
  • Breaking Change
  • Refactoring

Backend for public join requests (onboarding flow, Issue #308 – Subtask 1). Introduces the JoinRequest resource and persistence so that a confirmation flow can create records after email confirmation. No UI, no emails, no pre-confirmation logic in this PR. Security-focused: no public read on JoinRequest; idempotent confirm via unique constraint. Refactoring applies review feedback (narrow public API, server-set metadata only).

What has been changed?

  • Concept & docs

    • Added docs/onboarding-join-concept.md (design, data model, implementation plan for public join and later approval).
    • Updated docs/development-progress-log.md.
  • JoinRequest resource (lib/membership/join_request.ex)

    • New Ash resource with attributes: email, confirmation_token_hash, status, submitted_at, source, schema_version, payload, and audit fields (approved_at, rejected_at, reviewed_by_user_id).
    • Read: Single read action admin_read (primary); requires permission (e.g. admin). No public read – unauthenticated callers cannot list or read JoinRequests.
    • Create: create (for admins) and confirm (public, actor: nil). confirm accepts only email, confirmation_token_hash, payload; server sets status, submitted_at, source, schema_version via change module.
    • Policies: confirm allowed with actor_absent(); admin_read, create, update, destroy require HasPermission.
    • Unique identity on confirmation_token_hash for idempotency.
  • Change module (lib/membership/join_request/changes/set_confirm_server_metadata.ex)

    • Sets server-side metadata for confirm so clients cannot set status, source, etc.
  • Domain (lib/membership/membership.ex)

    • confirm_join_request/2: builds and runs confirm create; on unique-constraint violation (duplicate token) returns {:ok, nil} (idempotent, no record returned).
    • list_join_requests and get_join_request use admin_read.
  • Authorization

    • lib/mv/authorization/permission_sets.ex: JoinRequest added to admin permission set (read/create/update/destroy, scope :all).
  • Migrations

    • 20260220120000_add_join_requests.exs: table join_requests with unique index on confirmation_token_hash.
    • 20260220120001_alter_join_requests_schema_version_to_integer.exs: schema_version from bigint to integer.
  • Tests (test/mv/membership/join_request_test.exs)

    • Confirm with actor: nil; no public read (actor nil cannot read by id or list); generic create with actor nil forbidden; idempotency (second confirm with same token returns {:ok, nil}, single record); minimal attributes and required email. Count via list_join_requests(actor: system_actor) (no authorize?: false).
  • Gettext

    • Extract/merge run; obsolete entries removed from default.po (no new user-facing strings in this PR).

Definition of Done

Code Quality

  • No new technical depths
  • Linting passed
  • Documentation is added were needed

Accessibility

  • New elements are properly defined with html-tags
  • Colour contrast follows WCAG criteria
  • Aria labels are added when needed
  • Everything is accessible by keyboard
  • Tab-Order is comprehensible
  • All interactive elements have a visible focus

(N/A – no UI or HTML in this PR; backend only.)

Testing

  • Tests for new code are written
  • All tests pass
  • axe-core dev tools show no critical or major issues

(N/A – no frontend; axe not applicable.)

Additional Notes

  • Scope: This is Subtask 1 of the onboarding/join implementation plan in docs/onboarding-join-concept.md. Follow-up work (separate PRs): pre-confirmation store + confirmation email + route /confirm_join/:token (Subtask 2); admin join-form settings (Subtask 3); public /join page + anti-abuse (Subtask 4).
  • Security: No public read on JoinRequest; token cannot be used to read PII. Idempotency is implemented by catching the unique-constraint error on duplicate confirmation_token_hash and returning {:ok, nil} instead of exposing the existing record.
  • Review feedback applied: Public read removed; confirm accepts only client payload (email, confirmation_token_hash, payload); server metadata set in change; schema_version aligned to integer in DB; tests no longer use authorize?: false for counting (admin list used instead); Credo addressed (aliases for nested modules).
  • Suggested follow-up: When implementing the confirmation controller (Subtask 2), handle {:ok, nil} from confirm_join_request/2 as “already confirmed” and show the same success outcome as the first confirmation.
## Description of the implemented changes The changes were: - [ ] Bugfixing - [x] New Feature - [ ] Breaking Change - [x] Refactoring Backend for public join requests (onboarding flow, Issue #308 – Subtask 1). Introduces the `JoinRequest` resource and persistence so that a confirmation flow can create records after email confirmation. No UI, no emails, no pre-confirmation logic in this PR. Security-focused: no public read on JoinRequest; idempotent confirm via unique constraint. Refactoring applies review feedback (narrow public API, server-set metadata only). ## What has been changed? - **Concept & docs** - Added `docs/onboarding-join-concept.md` (design, data model, implementation plan for public join and later approval). - Updated `docs/development-progress-log.md`. - **JoinRequest resource** (`lib/membership/join_request.ex`) - New Ash resource with attributes: `email`, `confirmation_token_hash`, `status`, `submitted_at`, `source`, `schema_version`, `payload`, and audit fields (`approved_at`, `rejected_at`, `reviewed_by_user_id`). - **Read:** Single read action `admin_read` (primary); requires permission (e.g. admin). **No public read** – unauthenticated callers cannot list or read JoinRequests. - **Create:** `create` (for admins) and `confirm` (public, `actor: nil`). `confirm` accepts only `email`, `confirmation_token_hash`, `payload`; server sets `status`, `submitted_at`, `source`, `schema_version` via change module. - **Policies:** `confirm` allowed with `actor_absent()`; `admin_read`, `create`, `update`, `destroy` require `HasPermission`. - Unique identity on `confirmation_token_hash` for idempotency. - **Change module** (`lib/membership/join_request/changes/set_confirm_server_metadata.ex`) - Sets server-side metadata for `confirm` so clients cannot set `status`, `source`, etc. - **Domain** (`lib/membership/membership.ex`) - `confirm_join_request/2`: builds and runs `confirm` create; on unique-constraint violation (duplicate token) returns `{:ok, nil}` (idempotent, no record returned). - `list_join_requests` and `get_join_request` use `admin_read`. - **Authorization** - `lib/mv/authorization/permission_sets.ex`: JoinRequest added to **admin** permission set (read/create/update/destroy, scope :all). - **Migrations** - `20260220120000_add_join_requests.exs`: table `join_requests` with unique index on `confirmation_token_hash`. - `20260220120001_alter_join_requests_schema_version_to_integer.exs`: `schema_version` from bigint to integer. - **Tests** (`test/mv/membership/join_request_test.exs`) - Confirm with `actor: nil`; no public read (actor nil cannot read by id or list); generic `create` with actor nil forbidden; idempotency (second confirm with same token returns `{:ok, nil}`, single record); minimal attributes and required email. Count via `list_join_requests(actor: system_actor)` (no `authorize?: false`). - **Gettext** - Extract/merge run; obsolete entries removed from default.po (no new user-facing strings in this PR). ## Definition of Done ### Code Quality - [x] No new technical depths - [x] Linting passed - [x] Documentation is added were needed ### Accessibility - [x] New elements are properly defined with html-tags - [x] Colour contrast follows WCAG criteria - [x] Aria labels are added when needed - [x] Everything is accessible by keyboard - [x] Tab-Order is comprehensible - [x] All interactive elements have a visible focus *(N/A – no UI or HTML in this PR; backend only.)* ### Testing - [x] Tests for new code are written - [x] All tests pass - [x] axe-core dev tools show no critical or major issues *(N/A – no frontend; axe not applicable.)* ## Additional Notes - **Scope:** This is **Subtask 1** of the onboarding/join implementation plan in `docs/onboarding-join-concept.md`. Follow-up work (separate PRs): pre-confirmation store + confirmation email + route `/confirm_join/:token` (Subtask 2); admin join-form settings (Subtask 3); public `/join` page + anti-abuse (Subtask 4). - **Security:** No public read on JoinRequest; token cannot be used to read PII. Idempotency is implemented by catching the unique-constraint error on duplicate `confirmation_token_hash` and returning `{:ok, nil}` instead of exposing the existing record. - **Review feedback applied:** Public read removed; `confirm` accepts only client payload (`email`, `confirmation_token_hash`, `payload`); server metadata set in change; `schema_version` aligned to integer in DB; tests no longer use `authorize?: false` for counting (admin list used instead); Credo addressed (aliases for nested modules). - **Suggested follow-up:** When implementing the confirmation controller (Subtask 2), handle `{:ok, nil}` from `confirm_join_request/2` as “already confirmed” and show the same success outcome as the first confirmation.
simon added 5 commits 2026-02-20 18:26:04 +01:00
docs: concept self-sign-up
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/promote/production Build is failing
883e7a3e62
feat: join request backend
Some checks failed
continuous-integration/drone/push Build is failing
e7393e32d8
Merge remote-tracking branch 'origin/main' into feature/concept-web-form-308
Some checks failed
continuous-integration/drone/push Build is failing
bc9ea818eb
refactor: apply review notes
Some checks failed
continuous-integration/drone/push Build is failing
b41f005d9e
Some checks are pending
continuous-integration/drone/push Build is failing
continuous-integration/drone/promote/production
Required
This pull request has changes conflicting with the target branch.
  • priv/gettext/de/LC_MESSAGES/default.po
  • priv/gettext/en/LC_MESSAGES/default.po
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feature/concept-web-form-308:feature/concept-web-form-308
git checkout feature/concept-web-form-308
Sign in to join this conversation.
No description provided.