Skip to content

feat(week3): auth middleware, validators, and route wiring - #46

Merged
hnam10 merged 13 commits into
mainfrom
gary
May 25, 2026
Merged

feat(week3): auth middleware, validators, and route wiring#46
hnam10 merged 13 commits into
mainfrom
gary

Conversation

@humbeatbox

@humbeatbox humbeatbox commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • authenticate / requireRole / errorHandler and middleware
  • Zod schemas for auth and user endpoints
  • validate() / validateQuery() extracted to validators/shared.ts
  • Validation wired to all auth, user, and admin routes

Summary by CodeRabbit

  • New Features

    • Interactive API docs available at /api/docs
    • JWT Bearer authentication for protected endpoints
    • Role-based access controls for admin routes
    • Request validation with detailed, structured error responses
  • Bug Fixes / Improvements

    • Centralized error handling producing consistent JSON error codes
  • Chores

    • CI workflow updated to include an additional branch
    • .gitignore updated for local dev secrets
    • Backend dependency and workspace configuration updates

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Implements JWT auth and role guards, adds Zod request validation and shared validators, centralizes error handling, generates an OpenAPI spec and mounts Swagger UI at /api/docs, wires validation and JSDoc into auth/admin/user routes, and updates CI/infra/dependencies.

Changes

API authentication, validation, and documentation

Layer / File(s) Summary
Build infrastructure and dependencies
.github/workflows/ci.yml, .gitignore, backend/package.json, backend/pnpm-workspace.yaml
CI trigger extended to run on gary; .gitignore adds Claude local ignores; swagger-jsdoc and swagger-ui-express plus their @types added to backend deps; @scarf/scarf workspace allowBuilds set to false.
Swagger spec and server integration
backend/src/swagger.ts, backend/src/index.ts
Adds swagger.ts exporting swaggerSpec via swagger-jsdoc; server mounts Swagger UI at /api/docs and registers errorHandler after routers.
Error handling and validation utilities
backend/src/middleware/errorHandler.ts, backend/src/validators/shared.ts
Adds errorHandler mapping Zod and JWT errors to structured JSON responses; adds validate and validateQuery Zod middleware factories for request bodies and queries.
JWT authentication and role authorization
backend/src/middleware/authenticate.ts, backend/src/middleware/requireRole.ts
authenticate enforces Authorization: Bearer header, verifies JWT with process.env.JWT_ACCESS_SECRET, validates payload shape and attaches req.user, and maps token errors to 401 codes; requireRole returns 401 when unauthenticated and 403 when user role is not allowed.
Request validation schemas
backend/src/validators/auth.ts, backend/src/validators/users.ts
Concrete Zod schemas: loginSchema (Seneca email + password), refreshSchema, logoutSchema; user schemas for profile updates (at least one field), notifications (emailNotificationOptIn), user creation (role-conditional fields), and admin list query coercions/limits.
Route wiring and OpenAPI documentation
backend/src/routes/auth.ts, backend/src/routes/users.ts, backend/src/routes/admin/users.ts
Auth routes (/login, /refresh, /logout) and user routes (/me, /me/notifications) now run validation middleware; admin user routes add validateQuery(listUsersQuerySchema) and validate(createUserSchema) and include OpenAPI/JSDoc annotations; implementations remain stubs (501).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant authenticate
  participant requireRole
  participant RouteHandler
  participant errorHandler

  Client->>authenticate: HTTP request with Authorization: Bearer <token>
  authenticate->>authenticate: extract & verify JWT
  alt token valid
    authenticate->>RouteHandler: next() with req.user
    RouteHandler->>requireRole: route may invoke requireRole
    requireRole->>requireRole: check allowed roles
    alt role allowed
      requireRole->>RouteHandler: next() -> handler executes
    else forbidden
      requireRole->>Client: 403 FORBIDDEN
    end
  else token expired
    authenticate->>Client: 401 TOKEN_EXPIRED
  else invalid token
    authenticate->>Client: 401 INVALID_TOKEN
  end
  Note over errorHandler: Catches thrown errors and maps Zod/JWT/generic errors to structured JSON responses
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • 86unj/Foundit#37: Prior PR that introduced placeholder middleware and route stubs which this PR replaces with real JWT auth, role guards, validation, and error handling.

Poem

🐰 Tokens checked, schemas sown with care,
Error handlers tidy the lair,
Docs at /api/docs gleam in light,
Validators keep requests right,
A rabbit hops — secure and fair.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: implementing auth middleware (authenticate, requireRole, errorHandler), validators (Zod schemas), and wiring validation into routes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gary

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
backend/src/index.ts (1)

26-26: ⚡ Quick win

Gate Swagger UI exposure in production.

Line 26 mounts /api/docs for all environments; consider disabling or protecting it in production to reduce endpoint discovery surface.

🔐 Minimal env-based guard
-app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
+if (process.env.NODE_ENV !== 'production') {
+  app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/index.ts` at line 26, The Swagger UI route is always mounted via
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); — wrap that
mount in an environment-based guard (e.g., check process.env.NODE_ENV !==
'production' or require process.env.ENABLE_SWAGGER === 'true') or protect it
with an auth middleware so swaggerUi.setup/serve are not exposed in production;
update the code around the app.use call to only register swaggerUi.serve and
swaggerUi.setup(swaggerSpec) when the guard passes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/pnpm-workspace.yaml`:
- Line 4: Remove the allowlist entry for '`@scarf/scarf`' under allowBuilds (or
set it to false) so install-time lifecycle scripts aren't permitted;
alternatively, if you must keep the entry, ensure Scarf's telemetry is
explicitly disabled during install by setting scarfSettings.enabled=false or
SCARF_ANALYTICS=false when building (update the allowBuilds configuration and
any CI/build env config referencing '`@scarf/scarf`' accordingly).

In `@backend/src/middleware/authenticate.ts`:
- Around line 32-33: The middleware currently treats a missing JWT secret
(process.env.JWT_ACCESS_SECRET) as an INVALID_TOKEN client error when calling
jwt.verify (seen in the jwt.verify(token, process.env.JWT_ACCESS_SECRET!) usage
in authenticate.ts); change this to throw or forward a 500-class server error
when the secret is absent instead of returning 401/INVALID_TOKEN. Do the same
for the other verify usage around the refresh/second-verify block (lines 54-58
equivalent) so both jwt.verify calls check for a missing secret first and
return/forward an internal server error with a clear message before attempting
verification.
- Around line 32-45: The JWT payload returned by jwt.verify is being cast and
assigned directly to req.user without verifying the presence and types of
user_id, role, campus_id, and email; add a runtime check after jwt.verify (in
the authenticate middleware) that confirms payload is an object and that
payload.user_id and payload.campus_id and payload.email are non-empty strings
and payload.role is one of 'student'|'security'|'admin'; if the check fails,
treat it as an invalid token (return/throw the same error flow used for
INVALID_TOKEN) instead of attaching req.user, otherwise assign the validated
values to req.user.

In `@backend/src/routes/admin/users.ts`:
- Around line 73-103: The OpenAPI POST requestBody schema in the admin users
route currently lists a single static required array (email, firstName,
lastName, role, campusId) but the API enforces role-dependent required fields;
update the OpenAPI schema for the POST endpoint (the requestBody in the
admin/users route) to express those conditional requirements—for example replace
the flat schema with a oneOf (or discriminator) composed of role-specific
schemas for role=student (require studentNumber), role=security/administrative
(require employeeId), and role=admin (if different), or otherwise document the
conditional validation clearly in the schema description so generated clients
know which fields are required per role; ensure the role property is the
discriminator or included in each subschema so runtime validation matches the
docs.

In `@backend/src/routes/auth.ts`:
- Around line 7-32: Update the OpenAPI blocks for the auth routes to match
validator constraints and current runtime behavior: change the example email
domain in the /api/auth/login, /api/auth/refresh and /api/auth/logout schemas to
use the `@myseneca.ca` domain (as enforced by backend/src/validators/auth.ts), and
replace or augment the documented success responses with a 501 response note for
these endpoints to reflect the current "not implemented" handlers until real
implementations exist; ensure the route summaries (/api/auth/login,
/api/auth/refresh, /api/auth/logout) and their request/response schemas are
adjusted accordingly so docs and runtime remain consistent.

In `@backend/src/validators/users.ts`:
- Around line 64-67: The current isActive schema uses z.string().transform((v)
=> v === 'true') which coerces any string to false; replace it with an explicit
allowed-values schema such as z.enum(['true','false']).transform(v => v ===
'true').optional() (or z.union([z.literal('true'),
z.literal('false')]).transform(...).optional()) so inputs other than "true" or
"false" fail validation; update the isActive definition to use that enum/union
and keep the .optional() behavior.

---

Nitpick comments:
In `@backend/src/index.ts`:
- Line 26: The Swagger UI route is always mounted via app.use('/api/docs',
swaggerUi.serve, swaggerUi.setup(swaggerSpec)); — wrap that mount in an
environment-based guard (e.g., check process.env.NODE_ENV !== 'production' or
require process.env.ENABLE_SWAGGER === 'true') or protect it with an auth
middleware so swaggerUi.setup/serve are not exposed in production; update the
code around the app.use call to only register swaggerUi.serve and
swaggerUi.setup(swaggerSpec) when the guard passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d34b3ece-f6c6-483e-bb62-838632ec7e70

📥 Commits

Reviewing files that changed from the base of the PR and between 3e96bde and cdfba18.

⛔ Files ignored due to path filters (1)
  • backend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • .github/workflows/ci.yml
  • .gitignore
  • backend/package.json
  • backend/pnpm-workspace.yaml
  • backend/src/index.ts
  • backend/src/middleware/authenticate.ts
  • backend/src/middleware/errorHandler.ts
  • backend/src/middleware/requireRole.ts
  • backend/src/routes/admin/users.ts
  • backend/src/routes/auth.ts
  • backend/src/routes/users.ts
  • backend/src/swagger.ts
  • backend/src/validators/auth.ts
  • backend/src/validators/shared.ts
  • backend/src/validators/users.ts

Comment thread backend/pnpm-workspace.yaml Outdated
Comment thread backend/src/middleware/authenticate.ts Outdated
Comment thread backend/src/middleware/authenticate.ts Outdated
Comment on lines +73 to +103
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [email, firstName, lastName, role, campusId]
* properties:
* email:
* type: string
* example: student@myseneca.ca
* firstName:
* type: string
* lastName:
* type: string
* role:
* type: string
* enum: [student, security, admin]
* campusId:
* type: string
* format: uuid
* studentNumber:
* type: integer
* example: 123456789
* employeeId:
* type: string
* example: EMP00000001
* phone:
* type: string
* example: '4161234567'
* responses:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document role-dependent required fields in POST schema.

Line 79 defines a static required set, but this API uses role-specific constraints; generated clients may send payloads that pass docs but fail runtime validation.

📘 Suggested OpenAPI adjustment
 *         application/json:
 *           schema:
-*             type: object
-*             required: [email, firstName, lastName, role, campusId]
-*             properties:
+*             oneOf:
+*               - type: object
+*                 required: [email, firstName, lastName, role, campusId, studentNumber]
+*                 properties:
+*                   role:
+*                     type: string
+*                     enum: [student]
+*                   studentNumber:
+*                     type: integer
+*               - type: object
+*                 required: [email, firstName, lastName, role, campusId, employeeId]
+*                 properties:
+*                   role:
+*                     type: string
+*                     enum: [security, admin]
+*                   employeeId:
+*                     type: string
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/admin/users.ts` around lines 73 - 103, The OpenAPI POST
requestBody schema in the admin users route currently lists a single static
required array (email, firstName, lastName, role, campusId) but the API enforces
role-dependent required fields; update the OpenAPI schema for the POST endpoint
(the requestBody in the admin/users route) to express those conditional
requirements—for example replace the flat schema with a oneOf (or discriminator)
composed of role-specific schemas for role=student (require studentNumber),
role=security/administrative (require employeeId), and role=admin (if
different), or otherwise document the conditional validation clearly in the
schema description so generated clients know which fields are required per role;
ensure the role property is the discriminator or included in each subschema so
runtime validation matches the docs.

Comment thread backend/src/routes/auth.ts
Comment thread backend/src/validators/users.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/routes/admin/users.ts (1)

24-26: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

OpenAPI isActive type does not match validator expectation.

The OpenAPI spec declares isActive as type: boolean, but listUsersQuerySchema expects string literals "true" or "false". Query parameters arrive as strings in HTTP, and the validator explicitly uses .enum(['true', 'false']). API clients generated from this spec may send boolean values that fail validation.

📝 Suggested fix
       - in: query
         name: isActive
         schema:
-          type: boolean
+          type: string
+          enum: [true, false]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/admin/users.ts` around lines 24 - 26, The OpenAPI spec for
the query param `isActive` doesn't match the validator `listUsersQuerySchema`:
OpenAPI declares `isActive` as boolean while the validator expects string
literals `"true"`/`"false"`. Update the OpenAPI `isActive` declaration to type:
string and include enum: ['true','false'] (or otherwise mirror the validator) so
the spec matches `listUsersQuerySchema` and generated clients follow the same
string enum semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@backend/src/routes/admin/users.ts`:
- Around line 24-26: The OpenAPI spec for the query param `isActive` doesn't
match the validator `listUsersQuerySchema`: OpenAPI declares `isActive` as
boolean while the validator expects string literals `"true"`/`"false"`. Update
the OpenAPI `isActive` declaration to type: string and include enum:
['true','false'] (or otherwise mirror the validator) so the spec matches
`listUsersQuerySchema` and generated clients follow the same string enum
semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5543264d-58ed-4c2c-93c4-410c37566fb3

📥 Commits

Reviewing files that changed from the base of the PR and between cdfba18 and 9bf76d8.

📒 Files selected for processing (5)
  • backend/pnpm-workspace.yaml
  • backend/src/middleware/authenticate.ts
  • backend/src/routes/admin/users.ts
  • backend/src/routes/auth.ts
  • backend/src/validators/users.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/routes/admin/users.ts`:
- Around line 29-30: The OpenAPI schema for the isActive parameter currently
uses unquoted booleans (enum: [true, false]) while the validator expects string
literals (z.enum(['true','false'])); update the enum to use quoted strings
(enum: ['true', 'false']) in the route's parameter/schema for isActive so the
OpenAPI type: string matches the runtime validator z.enum(['true','false']) and
the values align.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b8451b3f-551c-4eb5-b305-62f55e4ad409

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf76d8 and 824d3c2.

📒 Files selected for processing (1)
  • backend/src/routes/admin/users.ts

Comment on lines +29 to +30
* type: string
* enum: [true, false]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how the current JSDoc YAML block is typed when parsed.
python - <<'PY'
import re
from pathlib import Path
import subprocess, sys

try:
    import yaml
except Exception:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "pyyaml"])
    import yaml

path = Path("backend/src/routes/admin/users.ts")
lines = path.read_text().splitlines()

start = next(i for i, l in enumerate(lines) if "`@openapi`" in l)
end = next(i for i in range(start + 1, len(lines)) if lines[i].strip() == "*/")

yaml_lines = []
for l in lines[start + 1:end]:
    m = re.match(r"\s*\*\s?(.*)$", l)
    if m:
        yaml_lines.append(m.group(1))

doc = yaml.safe_load("\n".join(yaml_lines))
params = doc["/api/admin/users"]["get"]["parameters"]
is_active = next(p for p in params if p.get("name") == "isActive")
enum_vals = is_active["schema"]["enum"]

print("enum values:", enum_vals)
print("enum value types:", [type(v).__name__ for v in enum_vals])
print("EXPECTED after fix: ['true', 'false'] and ['str', 'str']")
PY

Repository: 86unj/Foundit

Length of output: 176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the isActive OpenAPI JSDoc fragment
echo "---- OpenAPI JSDoc (isActive) ----"
rg -n "isActive" backend/src/routes/admin/users.ts
echo
# Print around the enum lines mentioned (best-effort)
nl -ba backend/src/routes/admin/users.ts | sed -n '15,45p'

echo
# Verify runtime validator expectation for isActive
echo "---- Validator for isActive ----"
rg -n "isActive" backend/src/validators/users.ts
echo
nl -ba backend/src/validators/users.ts | sed -n '40,95p'

Repository: 86unj/Foundit

Length of output: 183


🏁 Script executed:

#!/bin/bash
# Verify how the current JSDoc YAML block is typed when parsed.
python - <<'PY'
import re
from pathlib import Path
import subprocess, sys

try:
    import yaml
except Exception:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "pyyaml"])
    import yaml

path = Path("backend/src/routes/admin/users.ts")
lines = path.read_text().splitlines()

start = next(i for i, l in enumerate(lines) if "`@openapi`" in l)
end = next(i for i in range(start + 1, len(lines)) if lines[i].strip() == "*/")

yaml_lines = []
for l in lines[start + 1:end]:
    m = re.match(r"\s*\*\s?(.*)$", l)
    if m:
        yaml_lines.append(m.group(1))

doc = yaml.safe_load("\n".join(yaml_lines))
params = doc["/api/admin/users"]["get"]["parameters"]
is_active = next(p for p in params if p.get("name") == "isActive")
enum_vals = is_active["schema"]["enum"]

print("enum values:", enum_vals)
print("enum value types:", [type(v).__name__ for v in enum_vals])
PY

Repository: 86unj/Foundit

Length of output: 119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- backend/src/routes/admin/users.ts (isActive OpenAPI snippet) ----"
cat -n backend/src/routes/admin/users.ts | sed -n '15,45p'

echo
echo "---- backend/src/validators/users.ts (validator for isActive) ----"
rg -n "isActive" backend/src/validators/users.ts || true
echo
cat -n backend/src/validators/users.ts | sed -n '50,90p'

Repository: 86unj/Foundit

Length of output: 2207


Quote isActive enum literals to match the string validator and OpenAPI type

In backend/src/routes/admin/users.ts, enum: [true, false] is parsed as booleans, but the parameter schema declares type: string and the runtime validator expects string literals via z.enum(['true', 'false']). Quote the enum values.

📘 Proposed fix
 *           schema:
 *             type: string
-*           enum: [true, false]
+*           enum: ['true', 'false']
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* type: string
* enum: [true, false]
* type: string
* enum: ['true', 'false']
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/admin/users.ts` around lines 29 - 30, The OpenAPI schema
for the isActive parameter currently uses unquoted booleans (enum: [true,
false]) while the validator expects string literals (z.enum(['true','false']));
update the enum to use quoted strings (enum: ['true', 'false']) in the route's
parameter/schema for isActive so the OpenAPI type: string matches the runtime
validator z.enum(['true','false']) and the values align.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants