You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.person → Person.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").
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).
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.
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
Why
Part of the epic: #922. We want to invite people who already have a
Volunteerrow (created via the public form,POST /volunteer/legacyfromthe frontend) but have never registered a
Useraccount, so they can log inas
role: VOLUNTEER.Volunteerhas no direct link toUser— the only path isVolunteer.person→Person.users, and that's frequently empty. This isalready known/handled elsewhere: see the locale-fallback comment in
src/services/notify/email-template.ts("a volunteer with no User row toread a language preference from").
Design decisions (already made, don't re-litigate)
Userrow immediately —role: UserRole.VOLUNTEER,isActive: false, a random unusable placeholderpassword hash,
personIdset to the volunteer's existingPerson. Mirrorsthe existing verify-email pattern and keeps the accept step simple.
User/Persontables as-is.User.passwordisNOT NULL; do not add amigration 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
need4deed-sdkto that version before starting.sendVolunteerInvite, followingsrc/services/notify/events/email-verification.ts/email-password-reset.ts:{ id, email, type: "invite" }. Decide an expiry (passwordreset uses 60 min via
RESET_LIFESPAN_MS; an invite link probably wantsa longer window, e.g. several days — add a named constant to
src/config/constants.ts, don't hardcode).emailInviteManifestUrlinsrc/config/constants.ts) + a builtin fallback entry insrc/services/notify/builtin-content.ts, following the existingverification/reset email pattern exactly.
fastify.notifyinsrc/server/plugins/notify.ts.POST /auth/password-reset(branch on the JWT'stypeclaim to also flipisActive = truewhentype === "invite") or add a newPOST /auth/accept-inviteroute insrc/server/routes/auth.ts. Eitherway:
-active user with the correct
src/config/errorsubclass — no rawError, no try/catch relying on Fastify's own handler (repoconvention).
isActive = true.src/server/schema/auth.schema.tsconventions.Acceptance criteria
sendVolunteerInvitenotify event exists and sends via the existingverify-email SMTP transport
activates the account
it's called out explicitly in the PR description with the reason)