From c1db237cb4f4449ddc86c3bd888339edabf7857a Mon Sep 17 00:00:00 2001 From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:34:09 -0400 Subject: [PATCH 01/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35f2a8edc..2653c6b12 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Scaffolding +# Massachusetts Society for the Prevention of Cruelty to Animals From f78dc5b0a42296a8cd81836647edbb7a0e2c10cc Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 6 Sep 2026 10:00:08 -0400 Subject: [PATCH 02/20] Added Prettier CI check --- .github/workflows/prettier.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/prettier.yml diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 000000000..5204a17f8 --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,14 @@ +name: Prettier Check + +on: [push, pull_request] + +jobs: + prettier-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: yarn install --frozen-lockfile + - run: yarn prettier:check \ No newline at end of file From 626605f185a1f7737894bdc7e9b9140bf649a92c Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 6 Sep 2026 10:05:17 -0400 Subject: [PATCH 03/20] Fixed CI jobs --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ---- .github/workflows/ci-cd.yml | 118 ---------------------- package.json | 4 +- 3 files changed, 2 insertions(+), 140 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/workflows/ci-cd.yml diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7d6..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml deleted file mode 100644 index 6562108f1..000000000 --- a/.github/workflows/ci-cd.yml +++ /dev/null @@ -1,118 +0,0 @@ -# Note: -# This CICD requires AWS Amplify to be setup which tends to not be the case until the app is close to being deployed -# Therefore this is not useful for a the beginning chunk of development where everything is tested locally -# Therefore other pipelines have been made to test things locally, but once AWS Amplify is setup this can be used. - -# name: Legacy CI/CD - -# # First runs linter and tests all affected projects -# # Then, for each project that requires deployment, deploy-- is added -# # Environment variables are labelled _SHORT_DESCRIPTION - -# on: -# push: -# branches: ['main'] -# pull_request: -# branches: ['main'] -# workflow_dispatch: -# inputs: -# manual-deploy: -# description: 'App to Deploy' -# required: false -# default: '' - -# concurrency: -# # Never have two deployments happening at the same time (potential race condition) -# group: '{{ github.head_ref || github.ref }}' - -# jobs: -# pre-deploy: -# runs-on: ubuntu-latest -# outputs: -# affected: ${{ steps.should-deploy.outputs.affected }} -# steps: -# - uses: actions/checkout@v3 -# with: -# # We need to fetch all branches and commits so that Nx affected has a base to compare against. -# fetch-depth: 0 -# - name: Use Node.js 20 -# uses: actions/setup-node@v3 -# with: -# node-version: 20.x -# cache: 'yarn' - -# - name: Install Dependencies -# run: yarn install - -# # In any subsequent steps within this job (myjob) we can reference the resolved SHAs -# # using either the step outputs or environment variables: -# - name: Derive appropriate SHAs for base and head for `nx affected` commands -# uses: nrwl/nx-set-shas@v3 - -# - run: | -# echo "BASE: ${{ env.NX_BASE }}" -# echo "HEAD: ${{ env.NX_HEAD }}" - -# - name: Nx Affected Lint -# run: npx nx affected -t lint - -# # - name: Nx Affected Test -# # run: npx nx affected -t test - -# - name: Nx Affected Build -# run: npx nx affected -t build - -# - name: Determine who needs to be deployed -# id: should-deploy -# run: | -# echo "The following projects have been affected: [$(npx nx print-affected -t build --select=tasks.target.project)]"; -# echo "affected=$(npx nx print-affected -t build --select=tasks.target.project)" >> "$GITHUB_OUTPUT" - -# deploy-debug: -# needs: pre-deploy -# runs-on: ubuntu-latest -# steps: -# - name: Debug logs -# run: | -# echo "Manual Deploy: ${{github.event.inputs.manual-deploy}}"; -# echo "Affected Names: ${{needs.pre-deploy.outputs.affected}}"; -# echo "Event: ${{github.event_name}}"; -# echo "Ref: ${{github.ref}}"; -# echo "Will deploy?: ${{(github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'}}"; - -# deploy-frontend: -# needs: pre-deploy -# if: (contains(github.event.inputs.manual-deploy, 'c4c-ops-frontend') || contains(needs.pre-deploy.outputs.affected, 'scaffolding-frontend')) && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' -# runs-on: ubuntu-latest -# # For "simplicity", deployment settings are configured in the AWS Amplify Console -# # This just posts to a webhook telling Amplify to redeploy the main branch -# steps: -# - name: Tell Amplify to rebuild -# run: curl -X POST -d {} ${C4C_OPS_WEBHOOK_DEPLOY} -H "Content-Type:application/json" -# env: -# C4C_OPS_WEBHOOK_DEPLOY: ${{ secrets.C4C_OPS_WEBHOOK_DEPLOY }} - -# deploy-backend: -# needs: pre-deploy -# if: (contains(github.event.inputs.manual-deploy, 'c4c-ops-backend') || contains(needs.pre-deploy.outputs.affected, 'scaffolding-backend')) && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v3 -# - name: Use Node.js 16 -# uses: actions/setup-node@v3 -# with: -# node-version: 16.x -# cache: 'yarn' - -# - name: Install Dependencies -# run: yarn install - -# - run: npx nx build c4c-ops-backend --configuration production -# - name: default deploy -# uses: appleboy/lambda-action@master -# with: -# aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} -# aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} -# aws_region: ${{ secrets.AWS_REGION }} -# function_name: c4c-ops-monolith-lambda -# source: dist/apps/c4c-ops/c4c-ops-backend/main.js diff --git a/package.json b/package.json index 165c46263..78d2b9d9c 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "format:check": "prettier --no-error-on-unmatched-pattern --check apps/{frontend,backend}/src/**/*.{js,ts,tsx}", "format": "prettier --no-error-on-unmatched-pattern --write apps/{frontend,backend}/src/**/*.{js,ts,tsx}", - "lint:check": "eslint 'apps/frontend/src/**/*.{ts,tsx}' && eslint 'apps/backend/src/**/*.{ts,tsx}'", - "lint": "eslint 'apps/frontend/src/**/*.{ts,tsx}' --fix && eslint 'apps/backend/src/**/*.{ts,tsx}' --fix", + "lint:check": "eslint \"apps/frontend/src/**/*.{ts,tsx}\" && eslint \"apps/backend/src/**/*.{ts,tsx}\"", + "lint": "eslint \"apps/frontend/src/**/*.{ts,tsx}\" --fix && eslint \"apps/backend/src/**/*.{ts,tsx}\" --fix", "prepush": "yarn run format:check && yarn run lint:check", "prepush:fix": "yarn run format && yarn run lint", "prepare": "husky install", From e0c65628c1ec847de0fcaa746c23f975ac42920f Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 6 Sep 2026 10:21:26 -0400 Subject: [PATCH 04/20] CI jobs now target all branches, properly formatted everything --- .github/workflows/backend_build.yml | 9 ++-- .github/workflows/frontend_build.yml | 9 ++-- .github/workflows/jest.yml | 12 ++--- .github/workflows/lint.yml | 13 ++--- .github/workflows/prettier.yml | 9 +++- apps/backend/src/aws/cognito/README.md | 69 ++++++++++++++------------ apps/backend/src/aws/s3/README.md | 16 ++---- package.json | 16 +++--- 8 files changed, 73 insertions(+), 80 deletions(-) diff --git a/.github/workflows/backend_build.yml b/.github/workflows/backend_build.yml index 93c62cf20..a76b6319d 100644 --- a/.github/workflows/backend_build.yml +++ b/.github/workflows/backend_build.yml @@ -2,12 +2,12 @@ name: Backend build on: push: - branches: ['main'] + branches: ['**'] pull_request: - branches: ['main'] + branches: ['**'] jobs: - build: + backend-build: runs-on: ubuntu-latest steps: @@ -21,13 +21,12 @@ jobs: - name: Create .env file run: | - echo "AWS_SECRET_ACCESS_KEY=dummy" >> .env echo "AWS_ACCESS_KEY_ID=dummy" >> .env echo "AWS_REGION=us-east-2" >> .env - name: Install Dependencies - run: yarn install + run: yarn install --frozen-lockfile - name: Build backend run: npx nx build backend diff --git a/.github/workflows/frontend_build.yml b/.github/workflows/frontend_build.yml index fc7026362..7afc22cae 100644 --- a/.github/workflows/frontend_build.yml +++ b/.github/workflows/frontend_build.yml @@ -2,12 +2,12 @@ name: Frontend build on: push: - branches: ['main'] + branches: ['**'] pull_request: - branches: ['main'] + branches: ['**'] jobs: - build: + frontend-build: runs-on: ubuntu-latest steps: @@ -21,13 +21,12 @@ jobs: - name: Create .env file run: | - echo "AWS_SECRET_ACCESS_KEY=dummy" >> .env echo "AWS_ACCESS_KEY_ID=dummy" >> .env echo "AWS_REGION=us-east-2" >> .env - name: Install Dependencies - run: yarn install + run: yarn install --frozen-lockfile - name: Build frontend run: npx nx build frontend diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml index b892c48f0..0a6360488 100644 --- a/.github/workflows/jest.yml +++ b/.github/workflows/jest.yml @@ -2,13 +2,12 @@ name: Jest run on: push: - branches: ['main'] + branches: ['**'] pull_request: - branches: ['main'] + branches: ['**'] jobs: - build: - + jest-tests: runs-on: ubuntu-latest steps: @@ -22,14 +21,13 @@ jobs: - name: Create .env file run: | - echo "AWS_BUCKET_NAME=dummy" >> .env echo "AWS_SECRET_ACCESS_KEY=dummy" >> .env echo "AWS_ACCESS_KEY_ID=dummy" >> .env echo "AWS_REGION=us-east-2" >> .env - name: Install Dependencies - run: yarn install + run: yarn install --frozen-lockfile - - name: Run Jest tests + - name: Run Jest tests run: yarn test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f95b0e2e4..fabd3a593 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,9 +2,9 @@ name: Linter run on: push: - branches: ['main'] + branches: ['**'] pull_request: - branches: ['main'] + branches: ['**'] jobs: lint: @@ -12,8 +12,6 @@ jobs: steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Use Node.js 24.x uses: actions/setup-node@v4 @@ -22,10 +20,7 @@ jobs: cache: 'yarn' - name: Install Dependencies - run: yarn install - - - name: Derive appropriate SHAs for nx affected - uses: nrwl/nx-set-shas@v4 + run: yarn install --frozen-lockfile - name: Run linter - run: npx nx affected -t lint \ No newline at end of file + run: yarn lint:check \ No newline at end of file diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 5204a17f8..a7e300a75 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -1,6 +1,10 @@ name: Prettier Check -on: [push, pull_request] +on: + push: + branches: ['**'] + pull_request: + branches: ['**'] jobs: prettier-check: @@ -9,6 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24.x + cache: 'yarn' - run: yarn install --frozen-lockfile - run: yarn prettier:check \ No newline at end of file diff --git a/apps/backend/src/aws/cognito/README.md b/apps/backend/src/aws/cognito/README.md index 52b6c8591..2c453c57a 100644 --- a/apps/backend/src/aws/cognito/README.md +++ b/apps/backend/src/aws/cognito/README.md @@ -1,24 +1,30 @@ ## Scaffolding auth flow + Some key concepts you'll need to know are: -- **Authentication (authn)** = *"who are you?"* -> ex: distinguishing a known user from an unknown one -- **Authorization (authz)** = *"what are you allowed to do?"* -> ex: distinguishing admin access vs normal user access to routes + +- **Authentication (authn)** = _"who are you?"_ -> ex: distinguishing a known user from an unknown one +- **Authorization (authz)** = _"what are you allowed to do?"_ -> ex: distinguishing admin access vs normal user access to routes 1. **Unauthenticated user hits the app.** A user opens the frontend with no token. If they call a protected (Non Public) backend route, `CognitoJWTGuard` finds no `Authorization: Bearer ` header and responds `401 Unauthorized`. -2. **User authenticates with Cognito** The frontend sends the user's credentials to Cognito. Cognito verifies the credentials and *authenticates* the user. This happens entirely between the client and Cognito. Our backend is not involved and never sees the password. - - On the frontend this login flow is run by [AWS Amplify](https://docs.amplify.aws/). If auth is enabled, `apps/frontend/src/auth/auth.config.ts` (`configureAmplify()`) points Amplify at the user pool, and `apps/frontend/src/main.tsx` wraps the app in Amplify's `` login gate. +2. **User authenticates with Cognito** The frontend sends the user's credentials to Cognito. Cognito verifies the credentials and _authenticates_ the user. This happens entirely between the client and Cognito. Our backend is not involved and never sees the password. + + - On the frontend this login flow is run by [AWS Amplify](https://docs.amplify.aws/). If auth is enabled, `apps/frontend/src/auth/auth.config.ts` (`configureAmplify()`) points Amplify at the user pool, and `apps/frontend/src/main.tsx` wraps the app in Amplify's `` login gate. 3. **Cognito issues tokens.** On success, Cognito returns separate signed JWTs for the following: - - **ID token**: describes *who the user is* (identity claims), meant for the frontend. - - **access token**: the *authorization* credential, meant to be sent to backend APIs and checked by the `CognitoJWTGuard`. (See [Token validation](#token-validation)) + + - **ID token**: describes _who the user is_ (identity claims), meant for the frontend. + - **access token**: the _authorization_ credential, meant to be sent to backend APIs and checked by the `CognitoJWTGuard`. (See [Token validation](#token-validation)) - **refresh token**: used to obtain fresh ID/access tokens when they expire. 4. **Frontend calls the backend with the access token.** The client attaches it on every request as a header: `Authorization: Bearer `. This is done once, by an axios request interceptor in `apps/frontend/src/api/apiClient.ts`, so individual API methods never deal with tokens: + - it only sends the request with the access token when auth is enabled, so the scaffold still runs with no Cognito setup - `fetchAuthSession()` returns the cached access token and silently refreshes it when expired, so the 1 hour token lifetime needs no handling - if no one is signed in or if errors occur with fetching the auth session it sends the request unauthenticated and lets the guard answer `401` 5. **The Guard checks the token.** `CognitoJWTGuard` runs on every route (it's registered as a global `APP_GUARD`). For each request it: + - lets the request through immediately if auth is explicitly disabled (`AUTH_DISABLED=true`) or if the route is marked `@Public()` (intentional bypass) - extracts and verifies the Bearer token, then checks the RS256 signature against the pool's public keys (JWKS), the issuer, expiration, that `token_use === 'access'`, and that `client_id` matches our app client. @@ -28,33 +34,32 @@ Some key concepts you'll need to know are: So: **every route is protected by default, a request is allowed only if it carries a valid Cognito access token or if the route is marked `@Public()`, which skips the check entirely.** Public routes are for things that must work without a login, like health checks, webhooks, or the login entry point itself. -## QUICKSTART: +## QUICKSTART: Copy placeholders from the repo root `example.env` into `.env` (or your deployment secrets). The `COGNITO_*` variables drive **both** the backend and the frontend: -| Variable | Purpose | -|----------|---------| -| `AUTH_DISABLED` | Set to `true` to run with **no authentication at all** (**required** unless the `COGNITO_*` variables below are set). Only `true` and `false` are accepted. Never `true` in a deployed environment. | -| `COGNITO_USER_POOL_ID` | Your registered users in Cognito to authenticate with (**required** unless `AUTH_DISABLED=true`) | -| `COGNITO_CLIENT_ID` | The application you are building's own id linked to Cognito used to validate `client_id` on tokens (**required** unless `AUTH_DISABLED=true`) | -| `COGNITO_REGION` | AWS region (**optional**) — when unset it is derived from the user pool ID, which is formatted `_` (e.g. `us-east-2_abc123` → `us-east-2`). Set it explicitly only if your pool ID does not encode the region you want. | +| Variable | Purpose | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `AUTH_DISABLED` | Set to `true` to run with **no authentication at all** (**required** unless the `COGNITO_*` variables below are set). Only `true` and `false` are accepted. Never `true` in a deployed environment. | +| `COGNITO_USER_POOL_ID` | Your registered users in Cognito to authenticate with (**required** unless `AUTH_DISABLED=true`) | +| `COGNITO_CLIENT_ID` | The application you are building's own id linked to Cognito used to validate `client_id` on tokens (**required** unless `AUTH_DISABLED=true`) | +| `COGNITO_REGION` | AWS region (**optional**) — when unset it is derived from the user pool ID, which is formatted `_` (e.g. `us-east-2_abc123` → `us-east-2`). Set it explicitly only if your pool ID does not encode the region you want. | -`apps/frontend/vite.config.ts` re-exports the same values to the client bundle as `VITE_COGNITO_USER_POOL_ID`, `VITE_COGNITO_USER_POOL_CLIENT_ID`, and `VITE_COGNITO_REGION` at build time, so the client and server always share one source of truth (you never set the `VITE_` variables by hand). Because both sides read the same values, they can't drift out of sync: set the user pool ID and client ID and auth is enforced on the backend *and* the login UI appears on the frontend. +`apps/frontend/vite.config.ts` re-exports the same values to the client bundle as `VITE_COGNITO_USER_POOL_ID`, `VITE_COGNITO_USER_POOL_CLIENT_ID`, and `VITE_COGNITO_REGION` at build time, so the client and server always share one source of truth (you never set the `VITE_` variables by hand). Because both sides read the same values, they can't drift out of sync: set the user pool ID and client ID and auth is enforced on the backend _and_ the login UI appears on the frontend. -> [!IMPORTANT] -> **Running with auth off requires an explicit opt-in.** `AUTH_DISABLED=true` is the only thing that turns JWT enforcement off. Anything else that leaves the Cognito config unusable is treated as a misconfiguration and the application **refuses to start** +> [!IMPORTANT] > **Running with auth off requires an explicit opt-in.** `AUTH_DISABLED=true` is the only thing that turns JWT enforcement off. Anything else that leaves the Cognito config unusable is treated as a misconfiguration and the application **refuses to start** > [!WARNING] -> Disabling auth is a convenience for local development, **not** a safe production state. `example.env` ships with `AUTH_DISABLED=true` so that a fresh clone runs without any Cognito setup — remove it (or set it to `false`) as soon as you wire up a real user pool, and make sure it is never set in a deployed environment. If Cognito variables are present *and* `AUTH_DISABLED=true`, `CognitoModule` emits a second warning that the configuration is being ignored, which is the case worth grepping deploy logs for. +> Disabling auth is a convenience for local development, **not** a safe production state. `example.env` ships with `AUTH_DISABLED=true` so that a fresh clone runs without any Cognito setup — remove it (or set it to `false`) as soon as you wire up a real user pool, and make sure it is never set in a deployed environment. If Cognito variables are present _and_ `AUTH_DISABLED=true`, `CognitoModule` emits a second warning that the configuration is being ignored, which is the case worth grepping deploy logs for. > The frontend deliberately does **not** have a mirror of this flag as the security boundary is entirely server-side and it cannot enforce anything ## `AUTH_DISABLED` -The flag that gates the startup configuration check. +The flag that gates the startup configuration check. -Auth is opt-**out** because its disabled state *is* the vulnerability: every route served unauthenticated. +Auth is opt-**out** because its disabled state _is_ the vulnerability: every route served unauthenticated. - `CognitoModule.onModuleInit` **throws** `AuthConfigurationError` naming every missing variable, and the module lets it propagate so `main.ts` fails and the process exits. +`CognitoModule.onModuleInit` **throws** `AuthConfigurationError` naming every missing variable, and the module lets it propagate so `main.ts` fails and the process exits. `CognitoJWTGuard` reads a `null` config as "let this request through", and `getCognitoConfig()` returns `null` if and only if `AUTH_DISABLED=true`. @@ -63,11 +68,7 @@ Auth is opt-**out** because its disabled state *is* the vulnerability: every rou Add its name to `REQUIRED_ENV_VARS_WHEN_ENABLED` in `cognito.config.ts`: ```ts -export const REQUIRED_ENV_VARS_WHEN_ENABLED = [ - 'COGNITO_USER_POOL_ID', - 'COGNITO_CLIENT_ID', - 'COGNITO_MY_NEW_VAR', -] as const; +export const REQUIRED_ENV_VARS_WHEN_ENABLED = ['COGNITO_USER_POOL_ID', 'COGNITO_CLIENT_ID', 'COGNITO_MY_NEW_VAR'] as const; ``` ### Auth model @@ -93,6 +94,7 @@ export class AppModule {} New controllers are protected automatically. Opt out with `@Public()` (see below). Read the caller with `CognitoService.getUser(req)` or `req.user` after the guard runs. ### Public Routes + Use the `@Public()` decorator on routes that are technically protected, but don't require authentication. i.e. health checks, webhooks, or unauthenticated entry points: ```typescript @@ -126,18 +128,19 @@ Returns `null` if Cognito auth is disabled (`AUTH_DISABLED=true`) or if no verif ## Token validation The guard validates access tokens by + - JWKS: https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json - Signature: RS256; iss must match the pool. -- Expiration: exp is enforced automatically by jsonwebtoken.verify +- Expiration: exp is enforced automatically by jsonwebtoken.verify - token_use: must equal `access`. This is what rejects an ID token presented to the backend. - client_id: must equal `COGNITO_CLIENT_ID`. On access tokens the app client ID lives in the client_id claim - payload shape: `isAccessTokenPayload` rejects the token unless the required claims are present and well-typed — `sub`/`iss` are strings, `token_use === 'access'`, `client_id` is a string, `exp`/`iat` are numbers, and `cognito:groups` (if present) is an array of strings. > [!IMPORTANT] -> The scaffold accepts access tokens only by design. Backend APIs are resource servers and authorize requests using access tokens; ID tokens are for the frontend to establish who the user is. +> The scaffold accepts access tokens only by design. Backend APIs are resource servers and authorize requests using access tokens; ID tokens are for the frontend to establish who the user is. > [!WARNING] -> Common Confusion: Do not use the ID token for API authorization. ID tokens are intended for your client application to establish who the user is; passing them to a backend API exposes identity claims unnecessarily and confuses authentication with authorization. Backend APIs should validate access tokens only. +> Common Confusion: Do not use the ID token for API authorization. ID tokens are intended for your client application to establish who the user is; passing them to a backend API exposes identity claims unnecessarily and confuses authentication with authorization. Backend APIs should validate access tokens only. In `cognito.guard.ts`, `isAccessTokenPayload` validates the token type and claim shape, and the guard then checks the client id against the configured `COGNITO_CLIENT_ID`: @@ -149,13 +152,13 @@ if (payload.client_id !== config.clientId) { } ``` -> [!NOTE] -> `client_id` validation currently accepts a single client. If this pool ever serves -multiple app clients (e.g. a separate web and mobile app sharing one user pool), -change COGNITO_CLIENT_ID to accept a comma-separated list and validate membership -in that allowlist instead of simply an equality check. +> [!NOTE] > `client_id` validation currently accepts a single client. If this pool ever serves +> multiple app clients (e.g. a separate web and mobile app sharing one user pool), +> change COGNITO_CLIENT_ID to accept a comma-separated list and validate membership +> in that allowlist instead of simply an equality check. ## Helpful Resources for understanding Auth! + - The most amazing explanation of authn (OAUTH 2.0) and authz (OIDC) you'll ever watch: https://www.youtube.com/watch?v=996OiexHze0&t=2126s - Difference between id and access tokens: https://auth0.com/blog/id-token-access-token-what-is-the-difference/ - Using AWS to verify JWT: https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html diff --git a/apps/backend/src/aws/s3/README.md b/apps/backend/src/aws/s3/README.md index e3ab1d252..1a0780b94 100644 --- a/apps/backend/src/aws/s3/README.md +++ b/apps/backend/src/aws/s3/README.md @@ -48,11 +48,7 @@ export enum S3Buckets { **3. Add the env var name to `REQUIRED_ENV_VARS_WHEN_ENABLED`** (`aws-s3.module.ts`), so a missing value is reported at startup when `S3_ENABLED=true`: ```typescript -const REQUIRED_ENV_VARS_WHEN_ENABLED = [ - 'AWS_ACCESS_KEY_ID', - 'AWS_SECRET_ACCESS_KEY', - 'AWS_MY_BUCKET_NAME', -] as const; +const REQUIRED_ENV_VARS_WHEN_ENABLED = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_MY_BUCKET_NAME'] as const; ``` No change to `aws-s3.service.ts` is needed: its constructor resolves `process.env['AWS_' + bucket + '_BUCKET_NAME']` for every member of `S3Buckets`. A bucket whose env var is missing resolves to `''`, and any `upload()` to it throws `Missing required environment variable for S3 bucket: MY_BUCKET`. @@ -61,9 +57,9 @@ No change to `aws-s3.service.ts` is needed: its constructor resolves `process.en The credentials supplied via `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` must have: -| Permission | Required by | -|---|---| -| `s3:PutObject` | `upload()` | +| Permission | Required by | +| -------------- | ---------------- | +| `s3:PutObject` | `upload()` | | `s3:GetObject` | `getImageData()` | Scope permissions to only the buckets this application uses: @@ -75,9 +71,7 @@ Scope permissions to only the buckets this application uses: { "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject"], - "Resource": [ - "arn:aws:s3:::your-bucket-name/*" - ] + "Resource": ["arn:aws:s3:::your-bucket-name/*"] } ] } diff --git a/package.json b/package.json index 78d2b9d9c..449e7cdf6 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,17 @@ "scripts": { "format:check": "prettier --no-error-on-unmatched-pattern --check apps/{frontend,backend}/src/**/*.{js,ts,tsx}", "format": "prettier --no-error-on-unmatched-pattern --write apps/{frontend,backend}/src/**/*.{js,ts,tsx}", - "lint:check": "eslint \"apps/frontend/src/**/*.{ts,tsx}\" && eslint \"apps/backend/src/**/*.{ts,tsx}\"", - "lint": "eslint \"apps/frontend/src/**/*.{ts,tsx}\" --fix && eslint \"apps/backend/src/**/*.{ts,tsx}\" --fix", + "lint:check": "eslint apps/frontend --ext .ts,.tsx && eslint apps/backend --ext .ts,.tsx", + "lint": "eslint apps/frontend --ext .ts,.tsx --fix && eslint apps/backend --ext .ts,.tsx --fix", + "test": "jest --runInBand", "prepush": "yarn run format:check && yarn run lint:check", "prepush:fix": "yarn run format && yarn run lint", "prepare": "husky install", - "typeorm": "ts-node -r tsconfig-paths/register --project apps/backend/tsconfig.app.json ./node_modules/typeorm/cli.js", - "migration:generate": "yarn typeorm migration:generate apps/backend/src/migrations/$name -d apps/backend/src/data-source.ts", - "migration:run": "yarn typeorm migration:run -d apps/backend/src/data-source.ts", - "migration:revert": "yarn typeorm migration:revert -d apps/backend/src/data-source.ts", - "migration:create": "yarn typeorm migration:create apps/backend/src/migrations/$name", - "test": "jest" + "typeorm:migrate": "ts-node --project apps/backend/tsconfig.app.json -r dotenv/config ./node_modules/typeorm/cli.js migration:run -d apps/backend/src/config/typeorm.ts", + "typeorm:revert": "ts-node --project apps/backend/tsconfig.app.json -r dotenv/config ./node_modules/typeorm/cli.js migration:revert -d apps/backend/src/config/typeorm.ts", + "typeorm:create": "ts-node --project apps/backend/tsconfig.app.json -r dotenv/config ./node_modules/typeorm/cli.js migration:create", + "prettier:check": "prettier --check \"apps/**/*.{ts,tsx,js,jsx,json,md}\"", + "prettier:write": "prettier --write \"apps/**/*.{ts,tsx,js,jsx,json,md}\"" }, "private": true, "dependencies": { From de7d836290ff6560344eaa5bbaf238387f4c4f72 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 14 Sep 2026 00:28:45 -0400 Subject: [PATCH 05/20] Initial migration creation --- .../src/admin/admin.controller.spec.ts | 25 +++++ apps/backend/src/admin/admin.controller.ts | 7 ++ apps/backend/src/admin/admin.entity.ts | 30 ++++++ apps/backend/src/admin/admin.module.ts | 13 +++ apps/backend/src/admin/admin.service.spec.ts | 26 +++++ apps/backend/src/admin/admin.service.ts | 12 +++ apps/backend/src/app.module.ts | 11 ++- apps/backend/src/config/migrations.ts | 5 + apps/backend/src/config/typeorm.ts | 25 +++++ .../coordinators.controller.spec.ts | 25 +++++ .../coordinators/coordinators.controller.ts | 7 ++ .../src/coordinators/coordinators.entity.ts | 34 +++++++ .../src/coordinators/coordinators.module.ts | 13 +++ .../coordinators/coordinators.service.spec.ts | 26 +++++ .../src/coordinators/coordinators.service.ts | 12 +++ apps/backend/src/data-source.ts | 21 ---- .../src/matches/matches.controller.spec.ts | 25 +++++ .../backend/src/matches/matches.controller.ts | 7 ++ apps/backend/src/matches/matches.entity.ts | 31 ++++++ apps/backend/src/matches/matches.module.ts | 13 +++ .../src/matches/matches.service.spec.ts | 26 +++++ apps/backend/src/matches/matches.service.ts | 12 +++ apps/backend/src/matches/matches.types.ts | 8 ++ .../src/migrations/1754254886189-add_task.ts | 19 ---- .../1789354568323-InitialSchemas.ts | 98 +++++++++++++++++++ .../recommendations.controller.spec.ts | 27 +++++ .../recommendations.controller.ts | 7 ++ .../recommendations/recommendations.entity.ts | 18 ++++ .../recommendations/recommendations.module.ts | 13 +++ .../recommendations.service.spec.ts | 26 +++++ .../recommendations.service.ts | 12 +++ apps/backend/src/types.ts | 6 ++ .../volunteers/volunteers.controller.spec.ts | 25 +++++ .../src/volunteers/volunteers.controller.ts | 7 ++ .../src/volunteers/volunteers.entity.ts | 63 ++++++++++++ .../src/volunteers/volunteers.module.ts | 13 +++ .../src/volunteers/volunteers.service.spec.ts | 26 +++++ .../src/volunteers/volunteers.service.ts | 12 +++ .../src/volunteers/volunteers.types.ts | 6 ++ 39 files changed, 749 insertions(+), 43 deletions(-) create mode 100644 apps/backend/src/admin/admin.controller.spec.ts create mode 100644 apps/backend/src/admin/admin.controller.ts create mode 100644 apps/backend/src/admin/admin.entity.ts create mode 100644 apps/backend/src/admin/admin.module.ts create mode 100644 apps/backend/src/admin/admin.service.spec.ts create mode 100644 apps/backend/src/admin/admin.service.ts create mode 100644 apps/backend/src/config/migrations.ts create mode 100644 apps/backend/src/config/typeorm.ts create mode 100644 apps/backend/src/coordinators/coordinators.controller.spec.ts create mode 100644 apps/backend/src/coordinators/coordinators.controller.ts create mode 100644 apps/backend/src/coordinators/coordinators.entity.ts create mode 100644 apps/backend/src/coordinators/coordinators.module.ts create mode 100644 apps/backend/src/coordinators/coordinators.service.spec.ts create mode 100644 apps/backend/src/coordinators/coordinators.service.ts delete mode 100644 apps/backend/src/data-source.ts create mode 100644 apps/backend/src/matches/matches.controller.spec.ts create mode 100644 apps/backend/src/matches/matches.controller.ts create mode 100644 apps/backend/src/matches/matches.entity.ts create mode 100644 apps/backend/src/matches/matches.module.ts create mode 100644 apps/backend/src/matches/matches.service.spec.ts create mode 100644 apps/backend/src/matches/matches.service.ts create mode 100644 apps/backend/src/matches/matches.types.ts delete mode 100644 apps/backend/src/migrations/1754254886189-add_task.ts create mode 100644 apps/backend/src/migrations/1789354568323-InitialSchemas.ts create mode 100644 apps/backend/src/recommendations/recommendations.controller.spec.ts create mode 100644 apps/backend/src/recommendations/recommendations.controller.ts create mode 100644 apps/backend/src/recommendations/recommendations.entity.ts create mode 100644 apps/backend/src/recommendations/recommendations.module.ts create mode 100644 apps/backend/src/recommendations/recommendations.service.spec.ts create mode 100644 apps/backend/src/recommendations/recommendations.service.ts create mode 100644 apps/backend/src/types.ts create mode 100644 apps/backend/src/volunteers/volunteers.controller.spec.ts create mode 100644 apps/backend/src/volunteers/volunteers.controller.ts create mode 100644 apps/backend/src/volunteers/volunteers.entity.ts create mode 100644 apps/backend/src/volunteers/volunteers.module.ts create mode 100644 apps/backend/src/volunteers/volunteers.service.spec.ts create mode 100644 apps/backend/src/volunteers/volunteers.service.ts create mode 100644 apps/backend/src/volunteers/volunteers.types.ts diff --git a/apps/backend/src/admin/admin.controller.spec.ts b/apps/backend/src/admin/admin.controller.spec.ts new file mode 100644 index 000000000..51352b18d --- /dev/null +++ b/apps/backend/src/admin/admin.controller.spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminController } from './admin.controller'; +import { AdminService } from './admin.service'; + +describe('AdminController', () => { + let controller: AdminController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AdminController], + providers: [ + { + provide: AdminService, + useValue: {}, + }, + ], + }).compile(); + + controller = module.get(AdminController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/backend/src/admin/admin.controller.ts b/apps/backend/src/admin/admin.controller.ts new file mode 100644 index 000000000..e75d9a5ca --- /dev/null +++ b/apps/backend/src/admin/admin.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { AdminService } from './admin.service'; + +@Controller('admin') +export class AdminController { + constructor(private adminService: AdminService) {} +} diff --git a/apps/backend/src/admin/admin.entity.ts b/apps/backend/src/admin/admin.entity.ts new file mode 100644 index 000000000..a9825ef8c --- /dev/null +++ b/apps/backend/src/admin/admin.entity.ts @@ -0,0 +1,30 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity('admin_users') +export class AdminUser { + @PrimaryGeneratedColumn({ name: 'admin_id' }) + adminId!: number; + + @Column({ name: 'first_name', type: 'varchar', length: 255 }) + firstName!: string; + + @Column({ name: 'last_name', type: 'varchar', length: 255 }) + lastName!: string; + + @Column({ type: 'varchar', length: 20 }) + phone!: string; + + @Column({ + name: 'secondary_phone', + type: 'varchar', + length: 20, + nullable: true, + }) + secondaryPhone!: string | null; + + @Column({ type: 'varchar', length: 255 }) + email!: string; + + @Column({ type: 'boolean' }) + active!: boolean; +} diff --git a/apps/backend/src/admin/admin.module.ts b/apps/backend/src/admin/admin.module.ts new file mode 100644 index 000000000..3080bd0db --- /dev/null +++ b/apps/backend/src/admin/admin.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AdminUser } from './admin.entity'; +import { AdminController } from './admin.controller'; +import { AdminService } from './admin.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([AdminUser])], + controllers: [AdminController], + providers: [AdminService], + exports: [AdminService], +}) +export class AdminModule {} diff --git a/apps/backend/src/admin/admin.service.spec.ts b/apps/backend/src/admin/admin.service.spec.ts new file mode 100644 index 000000000..f9965e870 --- /dev/null +++ b/apps/backend/src/admin/admin.service.spec.ts @@ -0,0 +1,26 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { AdminService } from './admin.service'; +import { AdminUser } from './admin.entity'; + +describe('AdminService', () => { + let service: AdminService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AdminService, + { + provide: getRepositoryToken(AdminUser), + useValue: {}, + }, + ], + }).compile(); + + service = module.get(AdminService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/backend/src/admin/admin.service.ts b/apps/backend/src/admin/admin.service.ts new file mode 100644 index 000000000..458567efd --- /dev/null +++ b/apps/backend/src/admin/admin.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { AdminUser } from './admin.entity'; + +@Injectable() +export class AdminService { + constructor( + @InjectRepository(AdminUser) + private repo: Repository, + ) {} +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index df2d31bb0..a8c3074bc 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { ConfigModule } from '@nestjs/config'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import AppDataSource from './data-source'; +import typeorm from './config/typeorm'; import { CognitoModule } from './aws/cognito/cognito.module'; import { UsersModule } from './users/users.module'; @@ -11,8 +11,13 @@ import { UsersModule } from './users/users.module'; imports: [ ConfigModule.forRoot({ isGlobal: true, + load: [typeorm], + }), + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (configService: ConfigService) => + configService.getOrThrow('typeorm'), }), - TypeOrmModule.forRoot(AppDataSource.options), CognitoModule, UsersModule, ], diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts new file mode 100644 index 000000000..afb40b5d5 --- /dev/null +++ b/apps/backend/src/config/migrations.ts @@ -0,0 +1,5 @@ +import { InitialSchemas1789354568323 } from '../migrations/1789354568323-InitialSchemas'; + +const schemaMigrations = [InitialSchemas1789354568323]; + +export default schemaMigrations; diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts new file mode 100644 index 000000000..967c82205 --- /dev/null +++ b/apps/backend/src/config/typeorm.ts @@ -0,0 +1,25 @@ +import { registerAs } from '@nestjs/config'; +import { PluralNamingStrategy } from '../strategies/plural-naming.strategy'; +import { DataSource, DataSourceOptions } from 'typeorm'; +import schemaMigrations from './migrations'; +import 'pg'; + +const config = { + type: 'postgres', + host: `${process.env.DATABASE_HOST}`, + port: parseInt(`${process.env.DATABASE_PORT}`, 10), + database: `${process.env.DATABASE_NAME}`, + username: `${process.env.DATABASE_USERNAME}`, + password: `${process.env.DATABASE_PASSWORD}`, + autoLoadEntities: true, + synchronize: false, + namingStrategy: new PluralNamingStrategy(), + migrations: [...schemaMigrations], + ssl: + process.env.NODE_ENV === 'production' + ? { rejectUnauthorized: false } + : false, +}; + +export default registerAs('typeorm', () => config); +export const connectionSource = new DataSource(config as DataSourceOptions); diff --git a/apps/backend/src/coordinators/coordinators.controller.spec.ts b/apps/backend/src/coordinators/coordinators.controller.spec.ts new file mode 100644 index 000000000..5d8f27e43 --- /dev/null +++ b/apps/backend/src/coordinators/coordinators.controller.spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CoordinatorsController } from './coordinators.controller'; +import { CoordinatorsService } from './coordinators.service'; + +describe('CoordinatorsController', () => { + let controller: CoordinatorsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CoordinatorsController], + providers: [ + { + provide: CoordinatorsService, + useValue: {}, + }, + ], + }).compile(); + + controller = module.get(CoordinatorsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/backend/src/coordinators/coordinators.controller.ts b/apps/backend/src/coordinators/coordinators.controller.ts new file mode 100644 index 000000000..bbbddfd39 --- /dev/null +++ b/apps/backend/src/coordinators/coordinators.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { CoordinatorsService } from './coordinators.service'; + +@Controller('coordinators') +export class CoordinatorsController { + constructor(private coordinatorsService: CoordinatorsService) {} +} diff --git a/apps/backend/src/coordinators/coordinators.entity.ts b/apps/backend/src/coordinators/coordinators.entity.ts new file mode 100644 index 000000000..c0940f946 --- /dev/null +++ b/apps/backend/src/coordinators/coordinators.entity.ts @@ -0,0 +1,34 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Homebase } from '../types'; + +@Entity('foster_coordinators') +export class FosterCoordinator { + @PrimaryGeneratedColumn({ name: 'coordinator_id' }) + coordinatorId!: number; + + @Column({ name: 'first_name', type: 'varchar', length: 255 }) + firstName!: string; + + @Column({ name: 'last_name', type: 'varchar', length: 255 }) + lastName!: string; + + @Column({ type: 'varchar', length: 20 }) + phone!: string; + + @Column({ + name: 'secondary_phone', + type: 'varchar', + length: 20, + nullable: true, + }) + secondaryPhone!: string | null; + + @Column({ type: 'varchar', length: 255 }) + email!: string; + + @Column({ type: 'enum', enum: Homebase }) + homebase!: Homebase; + + @Column({ type: 'boolean' }) + active!: boolean; +} diff --git a/apps/backend/src/coordinators/coordinators.module.ts b/apps/backend/src/coordinators/coordinators.module.ts new file mode 100644 index 000000000..3ccf14884 --- /dev/null +++ b/apps/backend/src/coordinators/coordinators.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FosterCoordinator } from './coordinators.entity'; +import { CoordinatorsController } from './coordinators.controller'; +import { CoordinatorsService } from './coordinators.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([FosterCoordinator])], + controllers: [CoordinatorsController], + providers: [CoordinatorsService], + exports: [CoordinatorsService], +}) +export class CoordinatorsModule {} diff --git a/apps/backend/src/coordinators/coordinators.service.spec.ts b/apps/backend/src/coordinators/coordinators.service.spec.ts new file mode 100644 index 000000000..2003f702f --- /dev/null +++ b/apps/backend/src/coordinators/coordinators.service.spec.ts @@ -0,0 +1,26 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { CoordinatorsService } from './coordinators.service'; +import { FosterCoordinator } from './coordinators.entity'; + +describe('CoordinatorsService', () => { + let service: CoordinatorsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CoordinatorsService, + { + provide: getRepositoryToken(FosterCoordinator), + useValue: {}, + }, + ], + }).compile(); + + service = module.get(CoordinatorsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/backend/src/coordinators/coordinators.service.ts b/apps/backend/src/coordinators/coordinators.service.ts new file mode 100644 index 000000000..81fb74d9f --- /dev/null +++ b/apps/backend/src/coordinators/coordinators.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { FosterCoordinator } from './coordinators.entity'; + +@Injectable() +export class CoordinatorsService { + constructor( + @InjectRepository(FosterCoordinator) + private repo: Repository, + ) {} +} diff --git a/apps/backend/src/data-source.ts b/apps/backend/src/data-source.ts deleted file mode 100644 index e9c754811..000000000 --- a/apps/backend/src/data-source.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DataSource } from 'typeorm'; -import { PluralNamingStrategy } from './strategies/plural-naming.strategy'; -import * as dotenv from 'dotenv'; - -dotenv.config(); - -const AppDataSource = new DataSource({ - type: 'postgres', - host: process.env.NX_DB_HOST, - port: parseInt(process.env.NX_DB_PORT as string, 10), - username: process.env.NX_DB_USERNAME, - password: process.env.NX_DB_PASSWORD, - database: process.env.NX_DB_DATABASE, - entities: [], - migrations: ['apps/backend/src/migrations/*.js'], - // Setting synchronize: true shouldn't be used in production - otherwise you can lose production data - synchronize: false, - namingStrategy: new PluralNamingStrategy(), -}); - -export default AppDataSource; diff --git a/apps/backend/src/matches/matches.controller.spec.ts b/apps/backend/src/matches/matches.controller.spec.ts new file mode 100644 index 000000000..7e028c33e --- /dev/null +++ b/apps/backend/src/matches/matches.controller.spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MatchesController } from './matches.controller'; +import { MatchesService } from './matches.service'; + +describe('MatchesController', () => { + let controller: MatchesController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MatchesController], + providers: [ + { + provide: MatchesService, + useValue: {}, + }, + ], + }).compile(); + + controller = module.get(MatchesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/backend/src/matches/matches.controller.ts b/apps/backend/src/matches/matches.controller.ts new file mode 100644 index 000000000..9d62ae7e9 --- /dev/null +++ b/apps/backend/src/matches/matches.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { MatchesService } from './matches.service'; + +@Controller('matches') +export class MatchesController { + constructor(private matchesService: MatchesService) {} +} diff --git a/apps/backend/src/matches/matches.entity.ts b/apps/backend/src/matches/matches.entity.ts new file mode 100644 index 000000000..47d04f649 --- /dev/null +++ b/apps/backend/src/matches/matches.entity.ts @@ -0,0 +1,31 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { FosterVolunteer } from '../volunteers/volunteers.entity'; +import { MatchStatus } from './matches.types'; + +@Entity('matches') +export class Match { + @PrimaryGeneratedColumn({ name: 'match_id' }) + matchId!: number; + + @ManyToOne(() => FosterVolunteer, { nullable: false }) + @JoinColumn({ name: 'volunteer_id' }) + volunteer!: FosterVolunteer; + + @Column({ name: 'volunteer_id' }) + volunteerId!: number; + + @Column({ name: 'chameleon_animal_id', type: 'int' }) + chameleonAnimalId!: number; + + @Column({ type: 'enum', enum: MatchStatus }) + status!: MatchStatus; + + @Column({ name: 'denied_reason', type: 'text', nullable: true }) + deniedReason!: string | null; +} diff --git a/apps/backend/src/matches/matches.module.ts b/apps/backend/src/matches/matches.module.ts new file mode 100644 index 000000000..511bf15ff --- /dev/null +++ b/apps/backend/src/matches/matches.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Match } from './matches.entity'; +import { MatchesController } from './matches.controller'; +import { MatchesService } from './matches.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Match])], + controllers: [MatchesController], + providers: [MatchesService], + exports: [MatchesService], +}) +export class MatchesModule {} diff --git a/apps/backend/src/matches/matches.service.spec.ts b/apps/backend/src/matches/matches.service.spec.ts new file mode 100644 index 000000000..b2d520821 --- /dev/null +++ b/apps/backend/src/matches/matches.service.spec.ts @@ -0,0 +1,26 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { MatchesService } from './matches.service'; +import { Match } from './matches.entity'; + +describe('MatchesService', () => { + let service: MatchesService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MatchesService, + { + provide: getRepositoryToken(Match), + useValue: {}, + }, + ], + }).compile(); + + service = module.get(MatchesService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/backend/src/matches/matches.service.ts b/apps/backend/src/matches/matches.service.ts new file mode 100644 index 000000000..e91ccaf65 --- /dev/null +++ b/apps/backend/src/matches/matches.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Match } from './matches.entity'; + +@Injectable() +export class MatchesService { + constructor( + @InjectRepository(Match) + private repo: Repository, + ) {} +} diff --git a/apps/backend/src/matches/matches.types.ts b/apps/backend/src/matches/matches.types.ts new file mode 100644 index 000000000..d8f41e053 --- /dev/null +++ b/apps/backend/src/matches/matches.types.ts @@ -0,0 +1,8 @@ +export enum MatchStatus { + PENDING = 'Pending', + ACTIVE = 'Active', + COMPLETE = 'Complete', + DENIED = 'Denied', + WITHDRAWN = 'Withdrawn', + CANCELED = 'Canceled', +} diff --git a/apps/backend/src/migrations/1754254886189-add_task.ts b/apps/backend/src/migrations/1754254886189-add_task.ts deleted file mode 100644 index 450a64155..000000000 --- a/apps/backend/src/migrations/1754254886189-add_task.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddTask1754254886189 implements MigrationInterface { - name = 'AddTask1754254886189'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TYPE "public"."tasks_category_enum" AS ENUM('Draft', 'To Do', 'In Progress', 'Completed')`, - ); - await queryRunner.query( - `CREATE TABLE "task" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "description" character varying, "dateCreated" TIMESTAMP NOT NULL DEFAULT now(), "dueDate" TIMESTAMP, "labels" jsonb NOT NULL DEFAULT '[]', "category" "public"."tasks_category_enum" NOT NULL DEFAULT 'Draft', CONSTRAINT "PK_8d12ff38fcc62aaba2cab748772" PRIMARY KEY ("id"))`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "task"`); - await queryRunner.query(`DROP TYPE "public"."tasks_category_enum"`); - } -} diff --git a/apps/backend/src/migrations/1789354568323-InitialSchemas.ts b/apps/backend/src/migrations/1789354568323-InitialSchemas.ts new file mode 100644 index 000000000..a46fbb7b6 --- /dev/null +++ b/apps/backend/src/migrations/1789354568323-InitialSchemas.ts @@ -0,0 +1,98 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialSchemas1789354568323 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "homebase_enum" AS ENUM ('Boston', 'Cape Cod', 'Salem', 'Nevins Farm')`, + ); + await queryRunner.query( + `CREATE TYPE "foster_type_enum" AS ENUM ('Dog', 'Cat', 'Small Animals', 'Baby Bottle Animals')`, + ); + await queryRunner.query( + `CREATE TYPE "match_status_enum" AS ENUM ('Pending', 'Active', 'Complete', 'Denied', 'Withdrawn', 'Canceled')`, + ); + + await queryRunner.query(` + CREATE TABLE "foster_volunteers" ( + "volunteer_id" SERIAL PRIMARY KEY, + "first_name" varchar(255) NOT NULL, + "last_name" varchar(255) NOT NULL, + "phone" varchar(20) NOT NULL, + "secondary_phone" varchar(20), + "email" varchar(255) NOT NULL, + "address" varchar(255) NOT NULL, + "city" varchar(255) NOT NULL, + "zipcode" varchar(10) NOT NULL, + "homebase" "homebase_enum" NOT NULL, + "resident_animals" text NOT NULL, + "notes" text, + "foster_type" "foster_type_enum" NOT NULL, + "completed_canine_training" boolean, + "most_recent_waiver_signed" boolean NOT NULL, + "active" boolean NOT NULL + ) + `); + + await queryRunner.query(` + CREATE TABLE "foster_coordinators" ( + "coordinator_id" SERIAL PRIMARY KEY, + "first_name" varchar(255) NOT NULL, + "last_name" varchar(255) NOT NULL, + "phone" varchar(20) NOT NULL, + "secondary_phone" varchar(20), + "email" varchar(255) NOT NULL, + "homebase" "homebase_enum" NOT NULL, + "active" boolean NOT NULL + ) + `); + + await queryRunner.query(` + CREATE TABLE "admin_users" ( + "admin_id" SERIAL PRIMARY KEY, + "first_name" varchar(255) NOT NULL, + "last_name" varchar(255) NOT NULL, + "phone" varchar(20) NOT NULL, + "secondary_phone" varchar(20), + "email" varchar(255) NOT NULL, + "active" boolean NOT NULL + ) + `); + + await queryRunner.query(` + CREATE TABLE "matches" ( + "match_id" SERIAL PRIMARY KEY, + "volunteer_id" int NOT NULL, + "chameleon_animal_id" int NOT NULL, + "status" "match_status_enum" NOT NULL, + "denied_reason" text, + CONSTRAINT "fk_matches_volunteer" FOREIGN KEY ("volunteer_id") + REFERENCES "foster_volunteers" ("volunteer_id"), + CONSTRAINT "chk_denied_reason" CHECK ( + "status" != 'Denied' OR "denied_reason" IS NOT NULL + ) + ) + `); + + await queryRunner.query(` + CREATE TABLE "recommendations" ( + "volunteer_id" int NOT NULL, + "chameleon_animal_id" int NOT NULL, + "is_active" boolean NOT NULL DEFAULT true, + PRIMARY KEY ("volunteer_id", "chameleon_animal_id"), + CONSTRAINT "fk_recommendations_volunteer" FOREIGN KEY ("volunteer_id") + REFERENCES "foster_volunteers" ("volunteer_id") + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "recommendations"`); + await queryRunner.query(`DROP TABLE "matches"`); + await queryRunner.query(`DROP TABLE "admin_users"`); + await queryRunner.query(`DROP TABLE "foster_coordinators"`); + await queryRunner.query(`DROP TABLE "foster_volunteers"`); + await queryRunner.query(`DROP TYPE "match_status_enum"`); + await queryRunner.query(`DROP TYPE "foster_type_enum"`); + await queryRunner.query(`DROP TYPE "homebase_enum"`); + } +} diff --git a/apps/backend/src/recommendations/recommendations.controller.spec.ts b/apps/backend/src/recommendations/recommendations.controller.spec.ts new file mode 100644 index 000000000..748a2c3fc --- /dev/null +++ b/apps/backend/src/recommendations/recommendations.controller.spec.ts @@ -0,0 +1,27 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RecommendationsController } from './recommendations.controller'; +import { RecommendationsService } from './recommendations.service'; + +describe('RecommendationsController', () => { + let controller: RecommendationsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [RecommendationsController], + providers: [ + { + provide: RecommendationsService, + useValue: {}, + }, + ], + }).compile(); + + controller = module.get( + RecommendationsController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/backend/src/recommendations/recommendations.controller.ts b/apps/backend/src/recommendations/recommendations.controller.ts new file mode 100644 index 000000000..ed58b1def --- /dev/null +++ b/apps/backend/src/recommendations/recommendations.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { RecommendationsService } from './recommendations.service'; + +@Controller('recommendations') +export class RecommendationsController { + constructor(private recommendationsService: RecommendationsService) {} +} diff --git a/apps/backend/src/recommendations/recommendations.entity.ts b/apps/backend/src/recommendations/recommendations.entity.ts new file mode 100644 index 000000000..4593ac675 --- /dev/null +++ b/apps/backend/src/recommendations/recommendations.entity.ts @@ -0,0 +1,18 @@ +import { Entity, Column, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { FosterVolunteer } from '../volunteers/volunteers.entity'; + +@Entity('recommendations') +export class Recommendation { + @ManyToOne(() => FosterVolunteer, { nullable: false }) + @JoinColumn({ name: 'volunteer_id' }) + volunteer!: FosterVolunteer; + + @PrimaryColumn({ name: 'volunteer_id' }) + volunteerId!: number; + + @PrimaryColumn({ name: 'chameleon_animal_id', type: 'int' }) + chameleonAnimalId!: number; + + @Column({ name: 'is_active', type: 'boolean', default: true }) + isActive!: boolean; +} diff --git a/apps/backend/src/recommendations/recommendations.module.ts b/apps/backend/src/recommendations/recommendations.module.ts new file mode 100644 index 000000000..af7c536b8 --- /dev/null +++ b/apps/backend/src/recommendations/recommendations.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Recommendation } from './recommendations.entity'; +import { RecommendationsController } from './recommendations.controller'; +import { RecommendationsService } from './recommendations.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Recommendation])], + controllers: [RecommendationsController], + providers: [RecommendationsService], + exports: [RecommendationsService], +}) +export class RecommendationsModule {} diff --git a/apps/backend/src/recommendations/recommendations.service.spec.ts b/apps/backend/src/recommendations/recommendations.service.spec.ts new file mode 100644 index 000000000..5424b2ecd --- /dev/null +++ b/apps/backend/src/recommendations/recommendations.service.spec.ts @@ -0,0 +1,26 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { RecommendationsService } from './recommendations.service'; +import { Recommendation } from './recommendations.entity'; + +describe('RecommendationsService', () => { + let service: RecommendationsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RecommendationsService, + { + provide: getRepositoryToken(Recommendation), + useValue: {}, + }, + ], + }).compile(); + + service = module.get(RecommendationsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/backend/src/recommendations/recommendations.service.ts b/apps/backend/src/recommendations/recommendations.service.ts new file mode 100644 index 000000000..72688a007 --- /dev/null +++ b/apps/backend/src/recommendations/recommendations.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Recommendation } from './recommendations.entity'; + +@Injectable() +export class RecommendationsService { + constructor( + @InjectRepository(Recommendation) + private repo: Repository, + ) {} +} diff --git a/apps/backend/src/types.ts b/apps/backend/src/types.ts new file mode 100644 index 000000000..3c667a091 --- /dev/null +++ b/apps/backend/src/types.ts @@ -0,0 +1,6 @@ +export enum Homebase { + BOSTON = 'Boston', + CAPE_COD = 'Cape Cod', + SALEM = 'Salem', + NEVINS_FARM = 'Nevins Farm', +} diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts new file mode 100644 index 000000000..a3cdb3daa --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { VolunteersController } from './volunteers.controller'; +import { VolunteersService } from './volunteers.service'; + +describe('VolunteersController', () => { + let controller: VolunteersController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [VolunteersController], + providers: [ + { + provide: VolunteersService, + useValue: {}, + }, + ], + }).compile(); + + controller = module.get(VolunteersController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts new file mode 100644 index 000000000..3efd63ade --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { VolunteersService } from './volunteers.service'; + +@Controller('volunteers') +export class VolunteersController { + constructor(private volunteersService: VolunteersService) {} +} diff --git a/apps/backend/src/volunteers/volunteers.entity.ts b/apps/backend/src/volunteers/volunteers.entity.ts new file mode 100644 index 000000000..c49284a9d --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.entity.ts @@ -0,0 +1,63 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Homebase } from '../types'; +import { FosterType } from './volunteers.types'; + +@Entity('foster_volunteers') +export class FosterVolunteer { + @PrimaryGeneratedColumn({ name: 'volunteer_id' }) + volunteerId!: number; + + @Column({ name: 'first_name', type: 'varchar', length: 255 }) + firstName!: string; + + @Column({ name: 'last_name', type: 'varchar', length: 255 }) + lastName!: string; + + @Column({ type: 'varchar', length: 20 }) + phone!: string; + + @Column({ + name: 'secondary_phone', + type: 'varchar', + length: 20, + nullable: true, + }) + secondaryPhone!: string | null; + + @Column({ type: 'varchar', length: 255 }) + email!: string; + + @Column({ type: 'varchar', length: 255 }) + address!: string; + + @Column({ type: 'varchar', length: 255 }) + city!: string; + + @Column({ type: 'varchar', length: 10 }) + zipcode!: string; + + @Column({ type: 'enum', enum: Homebase }) + homebase!: Homebase; + + @Column({ name: 'resident_animals', type: 'text' }) + residentAnimals!: string; + + @Column({ type: 'text', nullable: true }) + notes!: string | null; + + @Column({ name: 'foster_type', type: 'enum', enum: FosterType }) + fosterType!: FosterType; + + @Column({ + name: 'completed_canine_training', + type: 'boolean', + nullable: true, + }) + completedCanineTraining!: boolean | null; + + @Column({ name: 'most_recent_waiver_signed', type: 'boolean' }) + mostRecentWaiverSigned!: boolean; + + @Column({ type: 'boolean' }) + active!: boolean; +} diff --git a/apps/backend/src/volunteers/volunteers.module.ts b/apps/backend/src/volunteers/volunteers.module.ts new file mode 100644 index 000000000..d3fef67b4 --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FosterVolunteer } from './volunteers.entity'; +import { VolunteersController } from './volunteers.controller'; +import { VolunteersService } from './volunteers.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([FosterVolunteer])], + controllers: [VolunteersController], + providers: [VolunteersService], + exports: [VolunteersService], +}) +export class VolunteersModule {} diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts new file mode 100644 index 000000000..fda546818 --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -0,0 +1,26 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { VolunteersService } from './volunteers.service'; +import { FosterVolunteer } from './volunteers.entity'; + +describe('VolunteersService', () => { + let service: VolunteersService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + VolunteersService, + { + provide: getRepositoryToken(FosterVolunteer), + useValue: {}, + }, + ], + }).compile(); + + service = module.get(VolunteersService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts new file mode 100644 index 000000000..df28421df --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { FosterVolunteer } from './volunteers.entity'; + +@Injectable() +export class VolunteersService { + constructor( + @InjectRepository(FosterVolunteer) + private repo: Repository, + ) {} +} diff --git a/apps/backend/src/volunteers/volunteers.types.ts b/apps/backend/src/volunteers/volunteers.types.ts new file mode 100644 index 000000000..5a3ea3de1 --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.types.ts @@ -0,0 +1,6 @@ +export enum FosterType { + DOG = 'Dog', + CAT = 'Cat', + SMALL_ANIMALS = 'Small Animals', + BABY_BOTTLE_ANIMALS = 'Baby Bottle Animals', +} From 0097512b1b9c0d91ef6a9017ecdff61893cfa9c3 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 14 Sep 2026 00:33:00 -0400 Subject: [PATCH 06/20] Removed unused files --- apps/backend/src/app.controller.spec.ts | 22 ------------------- apps/backend/src/app.controller.ts | 13 ----------- apps/backend/src/app.module.ts | 4 ---- apps/backend/src/app.service.spec.ts | 21 ------------------ apps/backend/src/app.service.ts | 8 ------- .../src/strategies/plural-naming.strategy.ts | 6 +---- 6 files changed, 1 insertion(+), 73 deletions(-) delete mode 100644 apps/backend/src/app.controller.spec.ts delete mode 100644 apps/backend/src/app.controller.ts delete mode 100644 apps/backend/src/app.service.spec.ts delete mode 100644 apps/backend/src/app.service.ts diff --git a/apps/backend/src/app.controller.spec.ts b/apps/backend/src/app.controller.spec.ts deleted file mode 100644 index de8007e18..000000000 --- a/apps/backend/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let app: TestingModule; - - beforeAll(async () => { - app = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - }); - - describe('getData', () => { - it('should return "Hello API"', () => { - const appController = app.get(AppController); - expect(appController.getData()).toEqual({ message: 'Hello API' }); - }); - }); -}); diff --git a/apps/backend/src/app.controller.ts b/apps/backend/src/app.controller.ts deleted file mode 100644 index dff210a84..000000000 --- a/apps/backend/src/app.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getData() { - return this.appService.getData(); - } -} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index a8c3074bc..f3f562fad 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -1,8 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import typeorm from './config/typeorm'; import { CognitoModule } from './aws/cognito/cognito.module'; import { UsersModule } from './users/users.module'; @@ -21,7 +19,5 @@ import { UsersModule } from './users/users.module'; CognitoModule, UsersModule, ], - controllers: [AppController], - providers: [AppService], }) export class AppModule {} diff --git a/apps/backend/src/app.service.spec.ts b/apps/backend/src/app.service.spec.ts deleted file mode 100644 index 42cf0a259..000000000 --- a/apps/backend/src/app.service.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Test } from '@nestjs/testing'; - -import { AppService } from './app.service'; - -describe('AppService', () => { - let service: AppService; - - beforeAll(async () => { - const app = await Test.createTestingModule({ - providers: [AppService], - }).compile(); - - service = app.get(AppService); - }); - - describe('getData', () => { - it('should return "Hello API"', () => { - expect(service.getData()).toEqual({ message: 'Hello API' }); - }); - }); -}); diff --git a/apps/backend/src/app.service.ts b/apps/backend/src/app.service.ts deleted file mode 100644 index cd8cedeff..000000000 --- a/apps/backend/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getData(): { message: string } { - return { message: 'Hello API' }; - } -} diff --git a/apps/backend/src/strategies/plural-naming.strategy.ts b/apps/backend/src/strategies/plural-naming.strategy.ts index bf3a51226..1a7c0a156 100644 --- a/apps/backend/src/strategies/plural-naming.strategy.ts +++ b/apps/backend/src/strategies/plural-naming.strategy.ts @@ -8,11 +8,7 @@ export class PluralNamingStrategy return userSpecifiedName || targetName.toLowerCase() + 's'; // Pluralize the table name } - columnName( - propertyName: string, - customName: string, - embeddedPrefixes: string[], - ): string { + columnName(propertyName: string): string { return propertyName; } From 6ab3eea90bd23ac56586a8a57a70cd6d0700ba9a Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 14 Sep 2026 00:40:31 -0400 Subject: [PATCH 07/20] Fixed CI --- .github/workflows/jest.yml | 33 +++---- apps/backend/src/app.module.ts | 2 - apps/backend/src/users/types.ts | 4 - apps/backend/src/users/user.entity.ts | 21 ----- apps/backend/src/users/users.controller.ts | 37 -------- apps/backend/src/users/users.module.ts | 13 --- apps/backend/src/users/users.service.ts | 91 ------------------- .../src/volunteers/volunteers.controller.ts | 8 ++ .../src/volunteers/volunteers.service.ts | 17 ++++ 9 files changed, 37 insertions(+), 189 deletions(-) delete mode 100644 apps/backend/src/users/types.ts delete mode 100644 apps/backend/src/users/user.entity.ts delete mode 100644 apps/backend/src/users/users.controller.ts delete mode 100644 apps/backend/src/users/users.module.ts delete mode 100644 apps/backend/src/users/users.service.ts diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml index 0a6360488..8fb2c7eaa 100644 --- a/.github/workflows/jest.yml +++ b/.github/workflows/jest.yml @@ -9,25 +9,16 @@ on: jobs: jest-tests: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Use NodeJS 24.x - uses: actions/setup-node@v4 - with: - node-version: 24.x - cache: 'yarn' - - - name: Create .env file - run: | - echo "AWS_BUCKET_NAME=dummy" >> .env - echo "AWS_SECRET_ACCESS_KEY=dummy" >> .env - echo "AWS_ACCESS_KEY_ID=dummy" >> .env - echo "AWS_REGION=us-east-2" >> .env - - - name: Install Dependencies - run: yarn install --frozen-lockfile - - - name: Run Jest tests - run: yarn test + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: 'yarn' + - run: yarn install --frozen-lockfile + - run: yarn test + env: + AWS_BUCKET_NAME: dummy + AWS_SECRET_ACCESS_KEY: dummy + AWS_ACCESS_KEY_ID: dummy + AWS_REGION: us-east-2 diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index f3f562fad..faede2748 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -3,7 +3,6 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import typeorm from './config/typeorm'; import { CognitoModule } from './aws/cognito/cognito.module'; -import { UsersModule } from './users/users.module'; @Module({ imports: [ @@ -17,7 +16,6 @@ import { UsersModule } from './users/users.module'; configService.getOrThrow('typeorm'), }), CognitoModule, - UsersModule, ], }) export class AppModule {} diff --git a/apps/backend/src/users/types.ts b/apps/backend/src/users/types.ts deleted file mode 100644 index dd9a359b9..000000000 --- a/apps/backend/src/users/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum Status { - ADMIN = 'ADMIN', - STANDARD = 'STANDARD', -} diff --git a/apps/backend/src/users/user.entity.ts b/apps/backend/src/users/user.entity.ts deleted file mode 100644 index 3224019c2..000000000 --- a/apps/backend/src/users/user.entity.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Entity, Column, ObjectIdColumn, ObjectId } from 'typeorm'; - -import type { Status } from './types'; - -@Entity() -export class User { - @Column({ primary: true }) - id: number; - - @Column() - status: Status; - - @Column() - firstName: string; - - @Column() - lastName: string; - - @Column() - email: string; -} diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts deleted file mode 100644 index e72930808..000000000 --- a/apps/backend/src/users/users.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - Body, - Controller, - Delete, - Get, - Param, - ParseIntPipe, - Post, -} from '@nestjs/common'; -import { UsersService } from './users.service'; -import { User } from './user.entity'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; - -@ApiTags('Users') -@ApiBearerAuth() -@Controller('users') -export class UsersController { - constructor(private usersService: UsersService) {} - - @Get('/:userId') - async getUser(@Param('userId', ParseIntPipe) userId: number): Promise { - return this.usersService.findOne(userId); - } - - @Delete('/:id') - removeUser(@Param('id') id: string) { - return this.usersService.remove(parseInt(id)); - } - - // Example endpoint for email sending (DO NOT DEPLOY THIS) - /* - @Post('/test-email') - sendTestEmail(@Body() body: { recipient: string }) { - return this.usersService.sendTestEmail(body.recipient); - } - */ -} diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts deleted file mode 100644 index 2474cf7f3..000000000 --- a/apps/backend/src/users/users.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { UsersController } from './users.controller'; -import { UsersService } from './users.service'; -import { User } from './user.entity'; -import { AWSSESModule } from '../aws/ses/email.module'; - -@Module({ - imports: [TypeOrmModule.forFeature([User]), AWSSESModule], - controllers: [UsersController], - providers: [UsersService], -}) -export class UsersModule {} diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts deleted file mode 100644 index 1f9ec22c1..000000000 --- a/apps/backend/src/users/users.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { User } from './user.entity'; -import { Status } from './types'; -import { EmailsService } from '../aws/ses/email.service'; -// import { SendEmailDTO } from '../aws/ses/sendEmail.dto'; - -@Injectable() -export class UsersService { - constructor( - @InjectRepository(User) private repo: Repository, - private emailsService: EmailsService, - ) {} - - // Example endpoint for email sending (DO NOT DEPLOY THIS) - /* - async sendTestEmail(recipient: string) { - const imageRes = await fetch('https://placehold.co/200x200/png'); - const imageBuffer = Buffer.from(await imageRes.arrayBuffer()); - - const dto: SendEmailDTO = { - toEmail: recipient, - ccEmails: ['cc@example.com'], - bccEmails: ['bcc@example.com'], - subject: 'Test email', - bodyHtml: '

This is a test email sent from UsersService.

', - attachments: [ - { - filename: 'sample.png', - content: imageBuffer, - }, - ], - }; - return this.emailsService.sendEmail(dto); - } - */ - - async create( - email: string, - firstName: string, - lastName: string, - status: Status = Status.STANDARD, - ) { - const userId = (await this.repo.count()) + 1; - const user = this.repo.create({ - id: userId, - status, - firstName, - lastName, - email, - }); - - return this.repo.save(user); - } - - findOne(id: number) { - if (!id) { - return null; - } - - return this.repo.findOneBy({ id }); - } - - find(email: string) { - return this.repo.find({ where: { email } }); - } - - async update(id: number, attrs: Partial) { - const user = await this.findOne(id); - - if (!user) { - throw new NotFoundException('User not found'); - } - - Object.assign(user, attrs); - - return this.repo.save(user); - } - - async remove(id: number) { - const user = await this.findOne(id); - - if (!user) { - throw new NotFoundException('User not found'); - } - - return this.repo.remove(user); - } -} diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index 3efd63ade..d2c246fc8 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -1,7 +1,15 @@ import { Controller } from '@nestjs/common'; import { VolunteersService } from './volunteers.service'; +// @ApiTags('Volunteers') +// @ApiBearerAuth() @Controller('volunteers') export class VolunteersController { constructor(private volunteersService: VolunteersService) {} + + // Example endpoint + // @Get('/:userId') + // async getUser(@Param('userId', ParseIntPipe) userId: number): Promise { + // return this.usersService.findOne(userId); + // } } diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index df28421df..1c6f17a61 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -9,4 +9,21 @@ export class VolunteersService { @InjectRepository(FosterVolunteer) private repo: Repository, ) {} + + // Example service functions + // find(email: string) { + // return this.repo.find({ where: { email } }); + // } + + // async update(id: number, attrs: Partial) { + // const user = await this.findOne(id); + + // if (!user) { + // throw new NotFoundException('User not found'); + // } + + // Object.assign(user, attrs); + + // return this.repo.save(user); + // } } From 36f72250eb85ddf80f62eab6c69830d471b9171d Mon Sep 17 00:00:00 2001 From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:43:32 -0400 Subject: [PATCH 08/20] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 2653c6b12..0d8230c64 100644 --- a/README.md +++ b/README.md @@ -97,10 +97,6 @@ The backend can expose [Swagger UI](https://github.com/swagger-api/swagger-ui) ( **Turn it off:** Unset `SWAGGER_ENABLED` or set it to `false`. -**TL TODOS:** -1. In [`apps/backend/src/main.ts`](apps/backend/src/main.ts), replace `[YOUR_APP_NAME]` and other `[TL]` strings, adjust `.addTag()` entries to match your controllers, -2. Add `@ApiBearerAuth()` onto the controller or handler for specifically protected routes. `addBearerAuth()` only states that the application supports authentication via HTTP Bearer in the Authorization header, it doesn't mark specific routes as protected. [Learn more](https://docs.nestjs.com/openapi/security#bearer-authentication) - **Decorators to know as you add routes!** (import from `@nestjs/swagger`): - `@ApiProperty()` / `@ApiPropertyOptional()`: on top of DTO fields From 280fb3701577a62a16404ae0673b63ad1d71ec68 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 14 Sep 2026 00:47:46 -0400 Subject: [PATCH 09/20] Fixed env variables --- apps/backend/src/config/typeorm.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index 967c82205..fba681c15 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -6,11 +6,11 @@ import 'pg'; const config = { type: 'postgres', - host: `${process.env.DATABASE_HOST}`, - port: parseInt(`${process.env.DATABASE_PORT}`, 10), - database: `${process.env.DATABASE_NAME}`, - username: `${process.env.DATABASE_USERNAME}`, - password: `${process.env.DATABASE_PASSWORD}`, + host: `${process.env.NX_DB_HOST}`, + port: parseInt(`${process.env.NX_DB_PORT}`, 10), + database: `${process.env.NX_DB_DATABASE}`, + username: `${process.env.NX_DB_USERNAME}`, + password: `${process.env.NX_DB_PASSWORD}`, autoLoadEntities: true, synchronize: false, namingStrategy: new PluralNamingStrategy(), From 422462e7dff7dc35ecff699870bb5e9b99a51077 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:48:55 -0400 Subject: [PATCH 10/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d8230c64..af6b04583 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ docker run --name scaffolding-db \ **Option B — pgAdmin / existing Postgres install:** -Create a new database (e.g. `jumpstart`) through pgAdmin or `psql`. +Create a new database (e.g. `mspca`) through pgAdmin or `psql`. **Configure connection strings:** From 996ef9492db3b59dc540004eba0733cf5d606ce5 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 14 Sep 2026 00:53:13 -0400 Subject: [PATCH 11/20] Added CI jobs into README --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index af6b04583..bec0e4aee 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,38 @@ To run both the frontend and backend with one command: nx run-many -t serve -p frontend backend ``` +## CI (GitHub Actions) + +On every push and pull request, GitHub Actions runs: + +- Prettier — checks formatting +- Lint - checks the linter +- Backend tests — runs Jest test suites + +### Local commands + +Prettier: + +``` +yarn prettier:check # verify formatting +yarn prettier:write # fix formatting +``` + +Lint: + +``` +yarn lint:check # runs linter +yarn lint # fixes linting issues +``` + +Backend tests: + +``` +yarn test +``` + +The locally hosted frontend and backend will update live as you make changes to the code. Happy coding! + ## Swagger The backend can expose [Swagger UI](https://github.com/swagger-api/swagger-ui) (built from an OpenAPI document via [`@nestjs/swagger`](https://docs.nestjs.com/openapi/introduction)) so you can browse and try HTTP routes without reading controller code first. From 2ca87253fbc39d0094ad9ab3e9f453b7f7e80895 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com> Date: Mon, 14 Sep 2026 00:54:10 -0400 Subject: [PATCH 12/20] Fix typo in README.md for Lint formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bec0e4aee..e9e22fd4a 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ nx run-many -t serve -p frontend backend On every push and pull request, GitHub Actions runs: - Prettier — checks formatting -- Lint - checks the linter +- Lint — checks the linter - Backend tests — runs Jest test suites ### Local commands From 6ae75c272d5ef7fd0ef5657ea3b0f42acc28d7d3 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 14 Sep 2026 21:08:52 -0400 Subject: [PATCH 13/20] Added id validation util function --- apps/backend/src/utils/validation.utils.spec.ts | 14 ++++++++++++++ apps/backend/src/utils/validation.utils.ts | 7 +++++++ 2 files changed, 21 insertions(+) create mode 100644 apps/backend/src/utils/validation.utils.spec.ts create mode 100644 apps/backend/src/utils/validation.utils.ts diff --git a/apps/backend/src/utils/validation.utils.spec.ts b/apps/backend/src/utils/validation.utils.spec.ts new file mode 100644 index 000000000..6b3908403 --- /dev/null +++ b/apps/backend/src/utils/validation.utils.spec.ts @@ -0,0 +1,14 @@ +import { BadRequestException } from '@nestjs/common'; +import { validateId } from './validation.utils'; + +describe('validateId', () => { + it('should not throw an error for a valid ID', () => { + expect(() => validateId(5, 'User')).not.toThrow(); + }); + + it('should throw BadRequestException for ID < 1', () => { + expect(() => validateId(0, 'User')).toThrow( + new BadRequestException('Invalid User ID'), + ); + }); +}); diff --git a/apps/backend/src/utils/validation.utils.ts b/apps/backend/src/utils/validation.utils.ts new file mode 100644 index 000000000..5c4f14a78 --- /dev/null +++ b/apps/backend/src/utils/validation.utils.ts @@ -0,0 +1,7 @@ +import { BadRequestException } from '@nestjs/common'; + +export function validateId(id: number, entityName: string): void { + if (!id || id < 1) { + throw new BadRequestException(`Invalid ${entityName} ID`); + } +} From a82852aaa77f34ac2f07111bb17f1db3db31f313 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 14 Sep 2026 21:29:13 -0400 Subject: [PATCH 14/20] Added volunteer coordinator relationship --- .../src/coordinators/coordinators.entity.ts | 9 ++++++++- .../backend/src/volunteers/volunteers.entity.ts | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/coordinators/coordinators.entity.ts b/apps/backend/src/coordinators/coordinators.entity.ts index c0940f946..1ce7a3c06 100644 --- a/apps/backend/src/coordinators/coordinators.entity.ts +++ b/apps/backend/src/coordinators/coordinators.entity.ts @@ -1,5 +1,6 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; import { Homebase } from '../types'; +import { FosterVolunteer } from '../volunteers/volunteers.entity'; @Entity('foster_coordinators') export class FosterCoordinator { @@ -31,4 +32,10 @@ export class FosterCoordinator { @Column({ type: 'boolean' }) active!: boolean; + + @OneToMany( + () => FosterVolunteer, + (volunteer) => volunteer.assignedCoordinator, + ) + assignedVolunteers!: FosterVolunteer[]; } diff --git a/apps/backend/src/volunteers/volunteers.entity.ts b/apps/backend/src/volunteers/volunteers.entity.ts index c49284a9d..45633aad2 100644 --- a/apps/backend/src/volunteers/volunteers.entity.ts +++ b/apps/backend/src/volunteers/volunteers.entity.ts @@ -1,6 +1,13 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; import { Homebase } from '../types'; import { FosterType } from './volunteers.types'; +import { FosterCoordinator } from '../coordinators/coordinators.entity'; @Entity('foster_volunteers') export class FosterVolunteer { @@ -60,4 +67,12 @@ export class FosterVolunteer { @Column({ type: 'boolean' }) active!: boolean; + + @ManyToOne( + () => FosterCoordinator, + (coordinator) => coordinator.assignedVolunteers, + { nullable: true }, + ) + @JoinColumn({ name: 'assigned_coordinator_id' }) + assignedCoordinator!: FosterCoordinator | null; } From 8048a768888c4996a3ee13d59fc783270327bd40 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:05:27 -0400 Subject: [PATCH 15/20] Ddb/test branch (#1) * Added comment * Fixed pre-push --- .husky/pre-push | 3 --- apps/backend/src/types.ts | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.husky/pre-push b/.husky/pre-push index 834ceb651..34edbdddd 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,6 +1,3 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" diff --git a/apps/backend/src/types.ts b/apps/backend/src/types.ts index 3c667a091..828006066 100644 --- a/apps/backend/src/types.ts +++ b/apps/backend/src/types.ts @@ -1,3 +1,4 @@ +// All commonly shared types go in here export enum Homebase { BOSTON = 'Boston', CAPE_COD = 'Cape Cod', From 1842757b4682b62a0128911deea0faa7eb2f5d34 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:58:10 -0400 Subject: [PATCH 16/20] Update PostgreSQL setup instructions in README Removed Docker setup instructions for PostgreSQL and updated password note. --- README.md | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/README.md b/README.md index e9e22fd4a..67697cf69 100644 --- a/README.md +++ b/README.md @@ -14,19 +14,6 @@ You can optionally install `nx` globally with `npm install -g nx` - if you don't This project uses PostgreSQL. You'll need a running Postgres instance before starting the backend. -**Option A — Docker (recommended for local dev):** - -```bash -docker run --name scaffolding-db \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=postgres \ - -e POSTGRES_DB=[DB NAME HERE] \ - -p 5432:5432 \ - -d postgres:15 -``` - -**Option B — pgAdmin / existing Postgres install:** - Create a new database (e.g. `mspca`) through pgAdmin or `psql`. **Configure connection strings:** @@ -41,7 +28,7 @@ cp example.env .env NX_DB_HOST=localhost NX_DB_PORT=5432 NX_DB_USERNAME=postgres -NX_DB_PASSWORD=postgres +NX_DB_PASSWORD=postgres (replace this with your actual PGAdmin4 password) NX_DB_DATABASE=[DB NAME HERE] ``` From 74dac85084f1cb530809bcd1fa21f4eb34cda30c Mon Sep 17 00:00:00 2001 From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com> Date: Mon, 21 Sep 2026 01:48:34 -0400 Subject: [PATCH 17/20] Delete .github/CODEOWNERS --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index ee529b859..000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @maxn990 From 2bed4ed1184a4f70c46a80ecd5c3895318fdd6c1 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com> Date: Mon, 21 Sep 2026 01:48:48 -0400 Subject: [PATCH 18/20] Delete .github/dependabot.yml --- .github/dependabot.yml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index ef97a9eaf..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,14 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file - -version: 2 -updates: - - package-ecosystem: "npm" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "weekly" - day: "monday" - time: "02:34" - timezone: "America/New_York" From 91a46b2ca123503ab434cf929e0f24ebc3e6b5da Mon Sep 17 00:00:00 2001 From: Shreeya Adhikari Date: Thu, 24 Sep 2026 15:16:55 -0400 Subject: [PATCH 19/20] created update volunteer dto --- .../src/volunteers/update-volunteer.dto.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 apps/backend/src/volunteers/update-volunteer.dto.ts diff --git a/apps/backend/src/volunteers/update-volunteer.dto.ts b/apps/backend/src/volunteers/update-volunteer.dto.ts new file mode 100644 index 000000000..a1317a24a --- /dev/null +++ b/apps/backend/src/volunteers/update-volunteer.dto.ts @@ -0,0 +1,61 @@ +import { FosterCoordinator as number } from '../coordinators/coordinators.entity'; +import { Homebase } from '../types'; +import { FosterType } from './volunteers.types'; +import { IsOptional, IsString, IsNumber, IsBoolean, IsEnum } from 'class-validator'; + + +export class UpdateVolunteerDto { + +@IsOptional() +@IsNumber() +volunteerId?: number; +@IsOptional() +@IsString() +firstName?: string; +@IsOptional() +@IsString() +lastName?: string; +@IsOptional() +@IsString() +phone?: string; +@IsOptional() +@IsString() +secondaryPhone?: string; +@IsOptional() +@IsString() +email?: string; +@IsOptional() +@IsString() +address?: string; +@IsOptional() +@IsString() +city?: string; +@IsOptional() +@IsString() +zipcode?: string; +@IsOptional() +@IsEnum(Homebase) +homebase?: Homebase; +@IsOptional() +@IsString() +residentAnimals?: string; +@IsOptional() +@IsString() +notes?: string; +@IsOptional() +@IsEnum(FosterType) +fosterType?: FosterType; +@IsOptional() +@IsBoolean() +completedCanineTraining?: boolean; +@IsOptional() +@IsBoolean() +mostRecentWaiverSigned?: boolean; +@IsOptional() +@IsBoolean() +active?: boolean; +@IsOptional() +@IsNumber() +assignedCoordinatorId?: number; + +} \ No newline at end of file From dc55c8951c48f5652748a012b55eeb5c71d79a41 Mon Sep 17 00:00:00 2001 From: Shreeya Adhikari Date: Sat, 26 Sep 2026 14:02:55 -0400 Subject: [PATCH 20/20] created deny match dto and added service logic --- apps/backend/src/matches/deny-match.dto.ts | 7 +++++++ apps/backend/src/matches/matches.service.ts | 22 +++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 apps/backend/src/matches/deny-match.dto.ts diff --git a/apps/backend/src/matches/deny-match.dto.ts b/apps/backend/src/matches/deny-match.dto.ts new file mode 100644 index 000000000..b4a15ed80 --- /dev/null +++ b/apps/backend/src/matches/deny-match.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class DenyMatchDto { + @IsNotEmpty() + @IsString() + deniedReason!: string; +} \ No newline at end of file diff --git a/apps/backend/src/matches/matches.service.ts b/apps/backend/src/matches/matches.service.ts index e91ccaf65..fab8ef157 100644 --- a/apps/backend/src/matches/matches.service.ts +++ b/apps/backend/src/matches/matches.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Match } from './matches.entity'; +import { MatchStatus } from './matches.types'; @Injectable() export class MatchesService { @@ -9,4 +10,25 @@ export class MatchesService { @InjectRepository(Match) private repo: Repository, ) {} + + async approveMatch(id: number) { + const match = await this.repo.findOneBy({ matchId: id }); + if (!match) { + return null; + } + + match.status = MatchStatus.COMPLETE; + return this.repo.save(match); + } + + async denyMatch(id: number, deniedReason: string) { + const match = await this.repo.findOneBy({ matchId: id }); + if (!match) { + return null; + } + + match.status = MatchStatus.DENIED; + match.deniedReason = deniedReason; + return this.repo.save(match); + } }