Skip to content

feat: invite flow — mint invite tokens for volunteers, accept-invite endpoint #920

Description

@arturasmckwcz

Why

Part of the epic: #922. We want to invite people who already have a
Volunteer row (created via the public form, POST /volunteer/legacy from
the frontend) but have never registered a User account, so they can log in
as role: VOLUNTEER.

Volunteer has no direct link to User — the only path is
Volunteer.personPerson.users, and that's frequently empty. This is
already known/handled elsewhere: see the locale-fallback comment in
src/services/notify/email-template.ts ("a volunteer with no User row to
read a language preference from").

Design decisions (already made, don't re-litigate)

  • Eager creation: at invite time, create the User row immediately —
    role: UserRole.VOLUNTEER, isActive: false, a random unusable placeholder
    password hash, personId set to the volunteer's existing Person. Mirrors
    the existing verify-email pattern and keeps the accept step simple.
  • No schema/migration changes anticipated — reuses the existing
    User/Person tables as-is. User.password is NOT NULL; do not add a
    migration to relax that — use a random unusable hash instead. If you find a
    reason a migration actually is needed, flag it here before writing one (see
    shared-rules.md: schema changes are deliberate, not incidental).

Scope

  1. Depends on the SDK contract from feat: accept-invite request/response contract for volunteer account linking sdk#212 being published — pin
    need4deed-sdk to that version before starting.
  2. New notify event, e.g. sendVolunteerInvite, following
    src/services/notify/events/email-verification.ts /
    email-password-reset.ts:
    • Sign a JWT { id, email, type: "invite" }. Decide an expiry (password
      reset uses 60 min via RESET_LIFESPAN_MS; an invite link probably wants
      a longer window, e.g. several days — add a named constant to
      src/config/constants.ts, don't hardcode).
    • Add a CDN manifest constant (e.g. emailInviteManifestUrl in
      src/config/constants.ts) + a builtin fallback entry in
      src/services/notify/builtin-content.ts, following the existing
      verification/reset email pattern exactly.
    • Wire it into fastify.notify in src/server/plugins/notify.ts.
  3. New accept-invite endpoint. During TODO review, decide whether to extend
    POST /auth/password-reset (branch on the JWT's type claim to also flip
    isActive = true when type === "invite") or add a new
    POST /auth/accept-invite route in src/server/routes/auth.ts. Either
    way:
    • Verify the JWT; reject expired/invalid tokens and tokens for an already
      -active user with the correct src/config/error subclass — no raw
      Error, no try/catch relying on Fastify's own handler (repo
      convention).
    • Set the real password (hashed) and set isActive = true.
    • Validate the request body against a new schema following
      src/server/schema/auth.schema.ts conventions.

Acceptance criteria

  • sendVolunteerInvite notify event exists and sends via the existing
    verify-email SMTP transport
  • Accept-invite endpoint verifies the token, sets a real password, and
    activates the account
  • Expired / invalid / already-used tokens return correct error responses
  • No database migration added (or, if one turned out to be necessary,
    it's called out explicitly in the PR description with the reason)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions