Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/backend-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
working-directory: backend

- name: Run linter
run: npm run lint || echo "Lint not configured, skipping"
run: npm run lint
working-directory: backend

# ─── Test ───────────────────────────────────────────────────────────────────
Expand All @@ -70,8 +70,10 @@ jobs:
run: npm ci
working-directory: backend

- name: Run unit tests
run: npm test -- --project=unit || echo "Tests passed or not configured"
# Run BOTH jest projects (unit + db) so the DB-backed session/auth/avatar
# tests gate the deploy. Uses mongodb-memory-server; no external services.
- name: Run tests
run: npm run test:ci
working-directory: backend
env:
NODE_ENV: test
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/frontend-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ jobs:
run: npm ci
working-directory: frontend

# Frontend lint is tolerant due to a large pre-existing lint backlog
# unrelated to current fixes. The build job below is the real gate.
- name: Run linter
run: npm run lint || echo "Lint not configured, skipping"
run: npm run lint || echo "Lint reported issues (non-blocking backlog)"
working-directory: frontend

# ─── Test ───────────────────────────────────────────────────────────────────
Expand Down
35 changes: 29 additions & 6 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@ jobs:
working-directory: backend

- name: Lint
run: npm run lint || echo "Lint not configured, skipping"
run: npm run lint
working-directory: backend

- name: Unit tests
run: npm test -- --project=unit || echo "No unit tests configured"
# Run BOTH jest projects (unit + db). The db project uses
# mongodb-memory-server (no external services) and holds the session-auth,
# /auth/*, and avatar-upload tests that protect these fixes.
- name: Tests
run: npm run test:ci
working-directory: backend
env:
NODE_ENV: test
Expand All @@ -70,12 +73,32 @@ jobs:
run: npm ci
working-directory: frontend

# Frontend lint is tolerant: eslint.config.js runs, but there is a large
# pre-existing lint backlog unrelated to these fixes. Kept non-blocking to
# avoid churn; the build step below is the real gate.
- name: Lint
run: npm run lint || echo "Lint not configured, skipping"
run: npm run lint || echo "Lint reported issues (non-blocking backlog)"
working-directory: frontend

- name: Tests
run: npm test -- --run || echo "No tests configured"
# Frontend has many pre-existing failing unit tests; the full run is kept
# non-blocking against that documented baseline.
- name: Tests (full, non-blocking baseline)
run: npm test -- --run || echo "Some frontend tests failing (non-blocking baseline)"
working-directory: frontend
env:
CI: true

# BLOCKING gate over the suites covering the auth/profile/onboarding fixes.
# These pass reliably and must not regress.
- name: Tests (changed suites, blocking)
run: npx vitest run src/context/__tests__/AuthContext.updateProfile.test.jsx src/services/__tests__/authService.endpoints.test.js
working-directory: frontend
env:
CI: true

# Build IS the real gate: a broken build must fail the PR (matches frontend-deploy.yml).
- name: Build
run: npm run build
working-directory: frontend
env:
CI: true
Expand Down
95 changes: 95 additions & 0 deletions .tasks/task-taskly-fixes/2026-08-30-133300-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Session-auth unification, Cloudinary avatar upload, onboarding/profile persistence, and CI gating (v1 review)

This change set repairs a Taskly deployment where "nothing worked." The core defect was an authentication mismatch: the frontend authenticates with Passport session cookies (`withCredentials`, no Bearer token), but every protected route ran through `authenticateToken`, which hard-rejected any request without a Bearer JWT. The fix teaches `authenticateToken` to fall back to the Passport session when Cognito is disabled, adds the missing `POST /api/upload/avatar` Cloudinary route the frontend already calls, wires the frontend profile/settings/onboarding flows to a new server-persisting `updateProfile`, reconciles frontend service calls that pointed at nonexistent `/auth/*` endpoints, and makes CI lint/test gating meaningful. A separately-broken error handler (a hard `SyntaxError` in the base branch) is also fixed.

Watch for: (1) **confirmed** — the new "Add User" form on `Users.jsx` calls the self-registration endpoint `POST /auth/register`, which server-side calls `req.logIn(newUser)` and replaces the admin's own session with the new user's; the admin is silently logged in as the account they just created. (2) **confirmed** — the backend CI gate runs only the `unit` Jest project (`test:unit`), so the DB-backed session/auth/avatar tests that actually protect this fix do not run in CI. (3) **likely** — 127 backend tests are skipped; the replacement coverage is real and revert-sensitive, but live-AWS/Cognito/S3 paths now have zero automated coverage in any environment.

**Verdict**: NEEDS_CHANGES

## High-level view

The auth fix is correctly gated: session fallback only engages when Cognito is not configured and a valid Passport session exists, so the Cognito and local-JWT paths are untouched and production security is not weakened. Passport's `deserializeUser` returns a full user document, so downstream `req.user._id`/`req.user.id` access behaves the same as the token paths.

The Cloudinary avatar route fails fast with `503 CLOUDINARY_NOT_CONFIGURED` before touching multer, and on success returns `{ data: { avatar, publicId } }` — exactly the shape `userService.uploadAvatarFile` reads.

The onboarding and profile changes target the reported symptoms directly: the `fullName`→`fullname` field rename fixes the pre-filled profile step, `updateProfile` persists to `PUT /users/profile` and rehydrates context state from the server response, and a null-`currentStepData` guard addresses the "step 4 empty" symptom. The backend already accepted `jobTitle`/`company`/`timezone`/`onboarding` (validation schema and model are unchanged here), so the frontend is now aligned with an existing contract rather than a new one.

The new "Add User" flow reuses the public self-registration endpoint, which auto-logs-in the created account and clobbers the caller's session — a functional regression for an admin-facing feature.

CI now makes the frontend build a hard gate and backend lint blocking (0 errors), but the backend test gate runs only the unit project — excluding the DB-backed tests that cover this fix — and the frontend test step is intentionally non-blocking against a large pre-existing failure backlog.

<details>
<summary>Issues (6)</summary>

1. **Add User hijacks admin session** — `Users.jsx handleAddUser` → `authService.register` → backend `req.logIn(newUser)` replaces the current session and overwrites `localStorage.user`; the admin becomes the new user. Use an admin-create path that does not log in the caller, or reload/re-authenticate the admin after creation.
2. **Backend CI gate skips the tests that protect this fix** — `pr-validation.yml` runs `npm run test:unit` (`--selectProjects unit`), but the session-auth, `/auth/*`, and avatar-upload tests live in the `db` project and never run in CI. Add the `db` project to the CI test step.
3. **No coverage for live Cognito/AWS/S3 paths** — 127 tests are skipped behind `RUN_AWS_INTEGRATION`/`RUN_ESM_MOCK_TESTS`; the session-path replacements are solid but the Cognito verifier, Lambda handler, and S3 presign paths now have zero automated coverage anywhere. Confirm this is acceptable or schedule an env-gated CI job.
4. **Frontend test step is non-blocking** — `pr-validation.yml` swallows Vitest failures (`|| echo ...`) against a 233-failing baseline, so a regression in the touched files won't fail the PR. Consider a targeted `vitest run` over the changed suites as a real gate.
5. **Avatar `fileFilter` trusts client mimetype** — `config/cloudinary.js` accepts anything with an `image/*` mimetype (client-controlled) and logs full file metadata to stdout on every upload. Pre-existing, but now reachable through the new route. (possible)
6. **Cloudinary config binds storage at module load** — `validateCloudinaryConfig()` is re-evaluated per request in the route guard, but the multer `storage` (Cloudinary vs memory) is chosen once at import; if env is configured after import the guard passes while storage is memory-only. (possible)

</details>

<details>
<summary>Details</summary>

### Session fallback in `authenticateToken`

When no `Bearer` header is present, the middleware honors the request only if `!isCognitoEnabled()` and `req.isAuthenticated()` and `req.user` are all true. `isCognitoEnabled()` keys off `COGNITO_USER_POOL_ID` + `COGNITO_CLIENT_ID`, so in a Cognito deployment the fallback is inert and a session cookie alone cannot authenticate — the right fail-closed posture for the trust boundary. The weaker session path is only reachable in the exact configuration (no Cognito) where the app is already session-based. (confirmed)

One residual asymmetry: a request that sends a non-Bearer `Authorization` header (e.g. Basic) while holding a valid session still falls through to the session branch and is honored. Harmless — the header is ignored — but a malformed auth header does not force a 401 when a session exists. (possible)

### Cloudinary avatar route and frontend contract

`POST /api/upload/avatar` guards on `validateCloudinaryConfig()` and returns `503 CLOUDINARY_NOT_CONFIGURED` before invoking multer, and `Profile.jsx` maps that status/code to a "try a preset avatar" message, closing the loop end to end. The route re-fetches the user with `User.findById(req.user._id)` rather than writing through the possibly-stale session document, and returns `{ data: { avatar, publicId } }` matching what `userService.uploadAvatarFile` reads. (confirmed)

The multer `fileFilter` in `config/cloudinary.js` accepts any `image/*` mimetype, which is client-controlled, and logs full file metadata to stdout on every request. Pre-existing code, but the new route is the first to route user uploads through it, so it is now a live surface. (possible)

### Onboarding and profile persistence

The reported "profile updates not working" and "step 4 empty" bugs trace to two concrete defects. First, the profile step bound to `formData.fullName` while the user model and API use `fullname`; the rename plus a `useEffect` that re-syncs the form when `user` loads means the field pre-fills and round-trips. Second, `renderStepContent` now guards `!currentStepData` before dereferencing it, and the flow resumes from `user.onboarding.currentStep`, addressing the blank-step symptom when `user` is momentarily null during auth bootstrap. Onboarding progress persists best-effort via `updateProfile({ onboarding })` and never throws, so a failed save cannot block navigation.

The backend side of this contract was already in place before this change — `updateProfileSchema` permits `jobTitle`/`company`/`timezone`/`onboarding` (with `.unknown(true)`), the controller maps those fields, and the model defines them. This diff aligns the frontend to an existing backend contract rather than introducing a new one, which lowers the risk of the persistence path. (confirmed)

### Add User flow reuses self-registration

`Users.jsx` gained an Add User form that calls `authService.register(...)`. Two coupled side effects make this the wrong endpoint for an admin-create action. On the client, `authService.register` writes the returned user into `localStorage.user` on success. On the server, the `register` controller calls `req.logIn(newUser, ...)` — confirmed by the passing test "registers a new user and auto-logs-in" — which issues a new session for the created account. The net effect: an admin who adds a user is silently re-authenticated as that new user, and their cached user record is overwritten. The Previous Onboards table (`GET /api/users`) is fine; the creation path is the problem. This needs either a dedicated admin-create endpoint that does not establish a session, or a client-side re-fetch of the admin's own session after creation. (confirmed)

### Error handler was a hard SyntaxError

The base-branch `errorHandler.js` commented out only the `console.error('Error:', {` line, leaving a dangling object literal with a `...spread` at statement position. `node --check` on the base file reports `SyntaxError: Unexpected token ':'` — the module cannot be imported, which would crash error-handling middleware registration at startup. The fix comments out the entire block. This is a genuine root-cause contributor to the "nothing works" report. (confirmed)

### Test coverage: what moved and what it now proves

The backend suite went from 88 failing to 0 failing / 75 passing / 127 skipped. The passing set is not hollow: `tests/routes/auth.test.js` drives the real session API through a supertest agent — register-then-auto-login, login by username and by email, `GET /api/auth/me` succeeding with a session cookie and **no Bearer token**, and logout invalidating the session. That last case is precisely the behavior the auth fallback introduces, so reverting the middleware change turns those tests red. `tests/routes/upload-avatar.test.js` covers success persistence, the 401-without-auth case, and the `503 CLOUDINARY_NOT_CONFIGURED` case. The two new frontend tests exercise `updateProfile` dispatch/persist and the reconciled `authService` endpoint set. This substantiates the "revert-sensitive replacement coverage" claim. (confirmed)

The gap is twofold. The skipped 127 tests gate live-AWS integration (`RUN_AWS_INTEGRATION`), Cognito/ESM-mock middleware and Lambda/S3 (`RUN_ESM_MOCK_TESTS`), and a secrets utility whose internals no longer exist — so the Cognito verifier, Lambda handler, and S3 presign code now have no automated coverage in any environment, not just CI. Separately, CI invokes only `test:unit` (`--selectProjects unit`), and the session/auth/avatar tests live in the `db` project — so the very tests that protect this fix do not execute on a PR. Adding the `db` project to the CI test step would make the gate match the coverage that was written. (confirmed for CI wiring; likely for the coverage-loss characterization)

### CI gating changes

Backend lint moved from `|| echo "skipping"` to blocking (`npm run lint`, 0 errors under the new flat `eslint.config.js`), and a frontend `npm run build` step is now a hard gate matching `frontend-deploy.yml`. The `eslint.config.js` layers `js.configs.recommended` and only downgrades noisy stylistic rules to warnings, so it still catches real errors rather than faking a green. The frontend lint and test steps remain non-blocking against a documented pre-existing backlog; that is defensible, but a regression in the touched frontend files would not fail the PR — only a build break would. (confirmed)

</details>

<details>
<summary>File map</summary>

- `backend/middleware/auth.js` — session fallback in `authenticateToken`, gated behind `!isCognitoEnabled()`.
- `backend/routes/upload.js` — new `POST /api/upload/avatar` Cloudinary route with 503 guard and user persistence.
- `backend/middleware/errorHandler.js` — fixes a base-branch `SyntaxError` (dangling object after commented log).
- `backend/server.js` — test-mode DB/session skip so tests use in-memory Mongo + default session store.
- `backend/eslint.config.js` — new ESLint v9 flat config; recommended rules, noisy rules as warnings.
- `backend/package.json`, `backend/jest.config.cjs` — `test:unit` script; `env-setup.js` setupFiles; `db` project ignores `tests/unit/`.
- `backend/tests/**` — session-based supertest suite, avatar-route tests, env-gated AWS/ESM-mock/secrets suites, rewritten email tests.
- `frontend/src/context/AuthContext.jsx` — new `updateProfile`; team/project bootstrap repointed to real services.
- `frontend/src/services/authService.js` — removed nonexistent `/auth/*` calls; `updateUserProfile` → `PUT /users/profile`.
- `frontend/src/services/teamService.js` — `/auth/me` response-shape handling for current-user id.
- `frontend/src/components/onboarding/OnboardingFlow.jsx` — `fullname` mapping, server persistence, resume, null-step guard.
- `frontend/src/pages/{Profile,Settings}.jsx` — route through `updateProfile`; 503 messaging on avatar upload.
- `frontend/src/pages/Users.jsx` — Add User form (reuses `/auth/register` — see Issue 1) and Previous Onboards table.
- `.github/workflows/pr-validation.yml` — blocking backend lint/test, non-blocking frontend lint/test, hard frontend build gate.
- `.tasks/task-taskly-fixes/**` — SPEC, context, and feature breakdown for the fix work.

Full diff: `git diff 238d11e`.

</details>
Loading
Loading