mitgliederverwaltung/docs/onboarding-join-concept.md
2026-02-20 13:40:31 +01:00

14 KiB
Raw Blame History

Onboarding & Join High-Level Concept

Status: Draft for design decisions and implementation specs
Scope: Prio 1 = public Join form; Step 2 = Vorstand approval. Invite-Link and OIDC JIT are out of scope and documented only as future entry paths.
Related: Issue #308, roles-and-permissions-architecture, page-permission-route-coverage.


1. Focus and Goals

  • Focus: Onboarding and initial data capture, not self-service editing of existing members.
  • Entry paths (vision):
    • Public Join form (Prio 1) unauthenticated submission.
    • Invite link (tokenized) later.
    • OIDC first-login (Just-in-Time Provisioning) later.
  • Admin control: All entry paths and their behaviour (e.g. which fields, approval required) shall be configurable by admins; MVP can start with sensible defaults.
  • Approval: A Vorstand (board) approval step is a direct follow-up (Step 2) after the public Join; the data model and flow must support it.

2. Prio 1: Public Join Page

2.1 Intent

  • Public page (e.g. /join): no login; anyone can open and submit.
  • Result is not a User or Member. Result is an onboarding / join request in status “submitted” (pending).
  • This keeps:
    • Public intake (abuse-prone) separate from identity and account creation (after approval / invite / OIDC).
    • Existing policies (e.g. UserMember linking, admin-only link) untouched until a defined “promotion” flow (e.g. after approval) creates User/Member.

2.2 User Flow (Prio 1)

  1. Unauthenticated user opens /join.
  2. Short explanation + form (what happens next: “We will review … you will hear from us”).
  3. Submit → “Please confirm your email” (confirmation email sent; no JoinRequest created yet).
  4. User clicks confirmation link → then the JoinRequest is created with status “submitted” and the user sees: “Thank you, we have received your request.”

Rationale (email confirmation first): Best practice for public signups is double opt-in: create the record only after the email address is verified. This reduces fake submissions, improves deliverability, and aligns with consent/compliance expectations (e.g. GDPR). The codebase already has a confirmation pattern (AshAuthentication user confirmation: token, email sender, confirm route); the same pattern can be reused for JoinRequest (store pending data with a short-lived token, send email, on link click create JoinRequest). Effort is moderate and acceptable.

Out of scope for Prio 1: Approval UI, account creation, OIDC, invite links.

2.3 Data Flow

  • Input: Only data explicitly allowed for the public form; field set is admin-configured (see §2.6). No internal or sensitive fields. Server-side allowlist: The set of accepted fields must be enforced on the server from the join-form settings (allowlist), not only in the UI, to prevent field injection or extra attributes from being stored.
  • Pre-confirmation: On form submit, store pending data with a short-lived confirmation token (e.g. in a transient store or a “pending” record); send confirmation email; do not create the JoinRequest yet. Implementation decision required: The choice between a transient store (e.g. ETS with TTL) and a dedicated DB table (e.g. “pending join” records) must be made in the implementation spec. Either way: define TTL/expiry, cleanup of unconfirmed data, and minimal PII retention (DSGVO). Document the decision and retention rules.
  • Post-confirmation: When the user clicks the confirmation link, create the JoinRequest resource with:
    • Submitted payload (minimal, well-defined schema; only whitelisted fields).
    • Status: submitted (later: approved, rejected).
    • Server-set metadata: submitted_at, locale, source (e.g. public_join), optional abuse signals (e.g. IP hash, spam score).
  • No creation of Member or User in Prio 1; promotion to Member/User happens in a later step (e.g. after approval).

2.4 Security

  • Public path: /join (and the confirmation route, e.g. /join/confirm/:token) must be treated as public. Add them to the public-path list used by the page-permission plug so unauthenticated access returns 200, not redirect to sign-in.
  • Abuse: Honeypot (MVP) plus rate limiting (MVP). Phoenix/Elixir has standard-friendly options: e.g. Hammer with Hammer.Plug (ETS backend, no Redis required), easily scoped to the join flow (e.g. by IP). Use both from the start for public intake. Before or during implementation, verify the chosen rate-limit library (e.g. Hammer) for current version, Phoenix/Plug compatibility, and suitability (e.g. ETS vs. Redis in multi-node setups).
  • Data: Minimal PII; no sensitive data on the public form; consider DSGVO and leak risk when extending fields. If abuse signals (e.g. IP hash, spam score) are stored: store only hashed or aggregated values (e.g. IP hash, not plain IP); ensure DSGVO/compliance when persisting any identifying or behavioural data.
  • Approval-only: No automatic User/Member creation from the join form; approval (Step 2) or other trusted path creates identity.
  • No system-actor fallback: The join and confirmation flow run without an authenticated user. When implementing backend actions (e.g. sending the confirmation email, creating the JoinRequest), do not use the system actor as a fallback for “missing actor”. Use an explicit unauthenticated/system context instead; never escalate privileges. See CODE_GUIDELINES §5.0.

2.5 Usability and UX

  • Clear heading and short copy (e.g. “Become a member / Submit request” and “What happens next”).
  • Form only as simple as needed (conversion vs. data hunger).
  • Success message: neutral, no promise of an account (“We will get in touch”).
  • Expired confirmation link: If the user clicks after the token has expired, show a clear message (e.g. “This link has expired”) and instruct them to submit the form again. Specify exact copy and behaviour in the implementation spec.
  • Re-send confirmation link: Out of scope for Prio 1; document as a possible future improvement (e.g. “Request new confirmation email” on the “Please confirm your email” or expired-link page).
  • Accessibility and i18n: same standards as rest of the app (e.g. labels, errors, Gettext).

2.6 Admin Configurability: Join Form Settings

  • Placement: Own section “Onboarding / Join” in global settings, above “Custom fields”, below “Vereinsdaten” (club data).
  • Join form enabled: Checkbox (e.g. join_form_enabled). When set, the public /join page is active and the following config applies.
  • Field selection: From all existing member fields (from Mv.Constants.member_fields()) and custom fields, the admin selects which fields appear on the join form. Stored as a list/set of field identifiers (no separate table); display in settings as a simple list, e.g. badges with X to remove (similar to the groups overview where members are added to a group: selected items as badges with remove). Adding fields: e.g. dropdown or modal to pick from remaining fields. Detailed UX for this settings subsection is to be specified in a separate subtask (no full table; keep overview clear).
  • Technically required fields: The only field that must always be required for the join flow is email (needed for the confirmation link and for creating a Member later). All other fields can be optional or marked as required per admin choice; implementation should support a “required” flag per selected join-form field.
  • Other: Which entry paths are enabled, approval workflow (who can approve) to be detailed in Step 2 and later specs.

3. Step 2: Vorstand Approval

  • Goal: Board (Vorstand) can review join requests (e.g. list “submitted”) and approve or reject.
  • Outcome of approval (admin-configurable):
    • Default: Approval creates Member only; no User is created. An admin can link a User later if needed.
    • Optional (configurable): If an option is set, approval may also create a User (e.g. for invite-to-set-password or similar). This behaviour depends on the configured login policies (OIDC, password, etc.) and is more complex; it is explicitly noted as an open topic for later and should not block Prio 1 or the basic approval flow. Implementation concepts for “approval + User creation” will be detailed when that option is implemented.
  • Permissions: Approval uses the existing permission set normal_user (e.g. role “Kassenwart”). No new permission set. The JoinRequest resource receives read and update (or dedicated approve/reject actions) for scope :all in normal_user, and the approval page (e.g. /join_requests or /onboarding/join_requests) is added to normal_users allowed pages. Users with normal_user can already create members; they can therefore approve join requests and create the resulting member. The organisation can assign the role that has normal_user to the person(s) who should perform approvals (e.g. Kassenwart or Vorstand, depending on configuration).

4. Future Entry Paths (Out of Scope Here)

  • Invite link (tokenized): Unique link per invitee; submission or account creation tied to token; no public form for that link.
  • OIDC first-login (JIT): First login via OIDC creates/links User and optionally Member from IdP data; no prior join form required for that path.
  • Both must be design-ready (e.g. shared “onboarding request” or “intake” abstraction) so they can attach to the same approval or creation pipeline later.

5. Evaluation of the Proposed Concept Draft

Adopted and reflected above:

  • Naming: Resource name JoinRequest (one resource, status + optional approval/rejection timestamps).
  • No User/Member from /join: Only a JoinRequest; record is created after email confirmation. Keeps abuse surface and policy complexity low.
  • Dedicated resource and action: New resource JoinRequest and public actions (e.g. one for “submit form” → store pending + send email; one for “confirm token” → create JoinRequest). Member/User domain unchanged.
  • Public path: /join and the confirmation route (e.g. /join/confirm/:token) in the public-path whitelist (page-permission plug).
  • Minimal data: Email is technically required; other fields from admin-configured join-form field set, with optional “required” per field.
  • Security: Honeypot + rate limiting (e.g. Hammer.Plug) in MVP; email confirmation before creating JoinRequest.
  • Tests: Unauthenticated GET /join → 200; confirm flow creates one JoinRequest; honeypot and rate limiting covered; public paths in plug test matrix.

Refinements in this document:

  • Approval (Vorstand) as Step 2; approval outcome configurable; User creation after approval noted as open for later.
  • Admin configurability: join form settings as own section (placement, field selection, required fields); detailed UX in a subtask.
  • Three entry paths (public, invite, OIDC) and their place in the roadmap made explicit.

6. Decisions and Open Points

Decided:

  • Email confirmation: JoinRequest is created only after the user clicks the confirmation link (double opt-in). Effort is acceptable; existing confirmation pattern in the app (AshAuthentication) can be reused.
  • Naming: JoinRequest.
  • Approval outcome: Admin-configurable. Default: approval creates Member only (no User). Optional “create User on approval” is possible but depends on login policies (OIDC, password, etc.) and is left as an open topic for later (to be specified when that option is implemented).
  • Rate limiting: Plan for honeypot + rate limiting from the start. Phoenix/Elixir ecosystem offers ready options (e.g. Hammer with Hammer.Plug, ETS backend) that are easy to integrate.
  • Settings: Own section “Onboarding / Join” in global settings (above custom fields, below club data). join_form_enabled plus field selection from all member fields and custom fields; display as list/badges with X to remove (UX reference: groups overview, add-members-to-group dialog). Email is the only technically required field; other required flags per field are configurable. Detailed UX for this subsection is to be specified in a separate subtask.
  • Approval permission: The approval UI and actions (list join requests, approve, reject) are gated by the existing permission set normal_user. JoinRequest read/update (or approve/reject actions) and the approval page are added to normal_user in PermissionSets; no new permission set or role required. The role that carries normal_user (e.g. Kassenwart) is the one that can perform approvals; the organisation assigns that role to the appropriate person(s).

7. Definition of Done (Prio 1)

  • Public /join page (and confirmation route) reachable without login (public paths configured and tested).
  • Flow: form submit → confirmation email → user clicks link → then one JoinRequest is created in status “submitted”; no User or Member is created before or by this flow.
  • Anti-abuse: honeypot and rate limiting (e.g. Hammer.Plug) implemented and tested.
  • Page-permission and routing tests updated (including public-path coverage).
  • Concept and decisions (§6) documented for use in implementation specs.

8. References

  • docs/roles-and-permissions-architecture.md Permission sets, roles, page permissions.
  • docs/page-permission-route-coverage.md Public paths, plug behaviour, tests.
  • lib/mv_web/plugs/check_page_permission.ex Public path list and redirect behaviour.
  • lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex Existing confirmation-email pattern (token, link, Mailer).
  • Hammer / Hammer.Plug (e.g. hexdocs.pm/hammer) Rate limiting for Phoenix/Plug; ETS backend does not require Redis.
  • Issue #308 Original feature/planning context.