Skip to content

MSPCA-20 Add POST /volunteers endpoint - #8

Open
Juwang110 wants to merge 8 commits into
MSPCA-7-activate-deactivate-account-by-idfrom
jw/mspca-20-create-volunteer-endpoint
Open

Juwang110 wants to merge 8 commits into
MSPCA-7-activate-deactivate-account-by-idfrom
jw/mspca-20-create-volunteer-endpoint

Conversation

@Juwang110

@Juwang110 Juwang110 commented Oct 5, 2026 •

Copy link
Copy Markdown

ℹ️ Issue

Closes MSPCA-20

Stacked on #6 (MSPCA-7). Merge #6 first, then retarget this to main.

📝 Description

Adds POST /api/volunteers so foster volunteers can sign up. It creates the user in Cognito with the FosterVolunteer role, then saves the volunteer in Postgres with the returned sub, as Pending until a coordinator approves them (MSPCA-17). Returns 201, 400 for invalid fields, or 409 if the email is taken.

  • Cognito first, then Postgres. If Cognito fails, no row is saved. If anything after Cognito creates the user fails (missing sub, group assignment, or the DB save), the Cognito user is deleted so the person can retry with the same email.
  • Emails are lowercased before the duplicate check and on save.
  • The route is @Public(), the first in the backend, since volunteers have no token yet when signing up.
  • status and mostRecentWaiverSigned can't be set by the client.
  • Adds a cognito_sub column (NOT NULL DEFAULT '') with a migration.

✔️ Verification

  • Service and controller tests cover signup, plus CognitoService cleanup, with Cognito mocked. All 233 backend tests pass, and lint, format, and typecheck are clean.
  • Ran against a real Cognito pool (us-east-2) and local Postgres with all migrations applied:
    • Valid signup with no auth header returns 201 with status: Pending, mostRecentWaiverSigned: true, and a cognitoSub. The user shows up in Cognito as FORCE_CHANGE_PASSWORD in the FosterVolunteer group, and its sub matches the row.
    • The same email again, or in different capitals, returns 409 from the Postgres check. With the row deleted, the same email returns 409 from Cognito and no row is saved.
    • Missing or invalid fields return 400, and status, cognitoSub, and mostRecentWaiverSigned in the body are ignored.
    • Other volunteer routes still return 401 without a token.
  • To test locally, the pool needs email as the username (not as an alias, or AdminCreateUser rejects it) and a FosterVolunteer group. Set AUTH_DISABLED=false, the COGNITO_* IDs, and AWS keys with AdminCreateUser, AdminAddUserToGroup, and AdminDeleteUser, then:
    curl -i -X POST localhost:3000/api/volunteers -H 'Content-Type: application/json' \
      -d '{"firstName":"Jane","lastName":"Doe","phone":"617-555-0100","email":"you@example.com","address":"350 S Huntington Ave","city":"Boston","zipcode":"02130","homebase":"Boston","residentAnimals":"One cat","fosterType":"Cat"}'

🏕️ (Optional) Future Work / Notes

N/A

Juwang110 and others added 2 commits October 3, 2026 11:19
…lunteer-endpoint

# Conflicts:
#	apps/backend/src/volunteers/volunteers.controller.spec.ts
#	apps/backend/src/volunteers/volunteers.controller.ts
#	apps/backend/src/volunteers/volunteers.service.spec.ts
#	apps/backend/src/volunteers/volunteers.service.ts
Creates the Cognito user with the FosterVolunteer role first, then saves
the volunteer as pending with the returned sub. A Cognito failure leaves
no Postgres row; duplicate emails return 409.

- Add CreateVolunteerDto with validation matching column lengths
- Add cognito_sub column (default '') and migration
- Mark the route @public() so volunteers can sign up without a token
- Register the ReplaceVolunteerActiveWithStatus migration from MSPCA-7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dburkhart07
dburkhart07 self-requested a review October 6, 2026 06:33
Juwang110 and others added 2 commits October 6, 2026 18:22
…unt-by-id' into jw/mspca-20-create-volunteer-endpoint

# Conflicts:
#	apps/backend/src/config/migrations.ts
#	apps/backend/src/volunteers/volunteers.controller.spec.ts
#	apps/backend/src/volunteers/volunteers.controller.ts
#	apps/backend/src/volunteers/volunteers.service.spec.ts
#	apps/backend/src/volunteers/volunteers.service.ts
Delete the Cognito user if the Postgres save, the group assignment, or the
sub lookup fails, so a retry with the same email is not rejected as a
duplicate. Lowercase volunteer emails before the duplicate check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juwang110
Juwang110 changed the base branch from main to MSPCA-7-activate-deactivate-account-by-id October 6, 2026 22:23
Juwang110 and others added 4 commits October 6, 2026 18:36
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unt-by-id' into jw/mspca-20-create-volunteer-endpoint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unt-by-id' into jw/mspca-20-create-volunteer-endpoint
@Juwang110
Juwang110 marked this pull request as ready for review October 7, 2026 22:03
@Juwang110
Juwang110 requested a review from Yurika-Kan October 7, 2026 22:03

@dburkhart07 dburkhart07 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

a lot of comments, but most are just nits. learned a lot about cognito for this. very detailed pr so far, ty justin! 🐊

@MaxLength(255)
lastName!: string;

@IsString()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we get rid of this and use @IsPhoneNumber like in the edit volunteers dto (this should be in the main branch as of writing this comment)

@MaxLength(20)
phone!: string;

@IsOptional()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same here


@IsOptional()
@IsString()
notes?: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can we add IsNotEmpty for this as well? This way we can't initialize the notes to "", but rather null instead if nothing is provided


@IsOptional()
@IsBoolean()
completedCanineTraining?: boolean;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this field is something that coordinators would fill in, so it may be best to not include this field right now, and initialize it to false.

}
}

async deleteUser(email: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this will make our lives easier for a ticket next week, so thanks for adding this!

it('creates the Cognito user with the FosterVolunteer role', async () => {
await service.create(dto);

expect(cognitoService.createUser).toHaveBeenCalledWith({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lets make sure this was only called once and repo.save only called once

});
});

it('saves the volunteer as pending with a signed waiver and the Cognito sub', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same thing here with only call and save once

},
);

it('lowercases the email before checking for duplicates and creating the user', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

only called once for createUser and save

);
});

it('deletes the Cognito user and rethrows when the Postgres save fails', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lets make sure createUser was called here too

);
});

it('logs the orphaned Cognito sub and rethrows the save error when the cleanup also fails', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

createUser called once, repo.save called once, deleteUser called once

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants