refactor: eliminate TypeScript any types across backend and frontend#122
Conversation
Implements type-safe authentication interfaces to eliminate any types in auth-critical code paths: - JwtPayload: JWT token payload structure with sub, username, iat, exp - AuthenticatedUser: User object attached to JWT-authenticated requests - AuthenticatedRequest: Express Request with authenticated user - RefreshTokenUser: User object for refresh token authentication - RefreshTokenRequest: Express Request with refresh token - ValidatedUser: User type without password (uses Omit utility type) These interfaces provide: - Type safety at JWT/auth boundaries - Prevention of password leakage through types - Clear separation between different auth contexts - Self-documenting authentication flow Related to #100 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Replace any types with proper TypeScript interfaces in auth module: - auth.service.ts: Use ValidatedUser and JwtPayload types - jwt.strategy.ts: Type validate() with JwtPayload and AuthenticatedUser - auth.controller.ts: Use AuthenticatedRequest and RefreshTokenRequest Security improvements: - Type-safe JWT payload handling prevents injection bugs - Impossible to accidentally expose user passwords via types - Compiler enforces correct user object structure Related to #100 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Update all controllers to use AuthenticatedRequest interface instead of req: any pattern: - org-inventory.controller.ts: Use AuthenticatedRequest for all endpoints - user-inventory.controller.ts: Use AuthenticatedRequest for user context - users.controller.ts: Type-safe request handling Also adds QueryParams interface for type-safe query parameter handling in inventory controllers. Benefits: - Type-safe access to req.user.userId and req.user.username - Compiler catches typos in request property access - Better IDE autocomplete and refactoring support Related to #100 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Replace Partial<T> & intersection types with explicit update DTOs in base-uex.repository.ts to avoid TypeORM's QueryDeepPartialEntity type complexity: - SoftDeleteUpdate interface: Enforces deleted: true (literal type) - ActivationUpdate interface: Handles active: boolean with modifiedById Implementation pattern: 1. Create typed DTO (e.g., SoftDeleteUpdate) 2. Construct updateData with explicit type 3. Use 'as unknown as QueryDeepPartialEntity<T>' at boundary Benefits: - Type-safe at construction (compiler catches mistakes) - Self-documenting (DTOs show intent) - Single assertion point (only at TypeORM boundary) - Prevents bugs (deleted: true literal prevents un-deletion) Technical notes: - Uses literal type 'deleted: true' to enforce soft-delete semantics - 'as unknown as' pattern is TypeScript-recommended for unavoidable casts - QueryDeepPartialEntity imported from internal TypeORM path (stable API) Related to #100 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Replace error.stack and error.message access on unknown types with
type-safe narrowing pattern across UEX sync modules:
- uex-sync.scheduler.ts: Type-safe error logging in all cron jobs
- locations-sync.service.ts: Proper error message extraction
- uex-items.client.ts: Remove duplicate errorResponse declaration
Pattern applied:
catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
this.logger.error(message, errorStack);
}
Benefits:
- Handles both Error objects and non-Error thrown values
- No TypeScript compilation errors on unknown type
- Defensive programming without paranoia
- Preserves stack traces when available
Related to #100
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace Record<string, any> with Record<string, unknown> in audit
logging system:
- audit-log.entity.ts: JSONB columns now use unknown type
- audit-logs.service.ts: CreateAuditLogDto uses unknown
- inventory-audit-log.entity.ts: Consistent metadata typing
Benefits:
- Forces type checking at access point (safer than any)
- Flexible metadata structure while maintaining type discipline
- Prevents accidental usage without type narrowing
- Better TypeScript strict mode compliance
Pattern:
const metadata: Record<string, unknown> = { ... };
// Must narrow type before use:
if (metadata && 'userId' in metadata) {
const userId = metadata.userId; // still requires narrowing
}
Related to #100
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements TokenCleanupService in the auth module: - Deletes refresh_tokens where revoked=true OR expiresAt < NOW() - Deletes password_resets where used=true OR expiresAt < NOW() - Logs deleted row counts and duration on success - Swallows errors to avoid crashing the process - Cron expression configurable via REFRESH_TOKEN_CLEANUP_CRON (default: 0 3 * * *) ScheduleModule is conditionally excluded in test env (NODE_ENV=test) so the @Cron decorator is never registered during tests. Closes #98
Backend: - ESLint config updated to enforce @typescript-eslint/no-explicit-any - Typed JWT auth interfaces for strategies and guards - AuthenticatedRequest type replacing any in controllers - Typed DTOs for UEX repository update operations - Proper type narrowing in error handling (error instanceof Error) - Record<string, unknown> for audit log metadata Frontend: - ESLint config updated to enforce no-explicit-any - Inventory components and pages fully typed - Services, utils, and focusController typed - Test files updated to match typed signatures Closes #100
There was a problem hiding this comment.
Pull request overview
This PR removes explicit any usage across the backend (NestJS/TypeORM) and frontend (React/MUI) to tighten type safety at auth/request boundaries and other data-flow edges, and adds/adjusts ESLint rules to prevent regressions.
Changes:
- Replaces
anyin auth + request handling with typed JWT payload/user/request interfaces across backend controllers, guards/strategies, and services. - Tightens typing in UEX sync/client error handling and UEX repository update paths (moving from
anytounknown+ narrowing / typed DTOs). - Enables
@typescript-eslint/no-explicit-anyin the frontend and (partially) in backend ESLint configs.
Reviewed changes
Copilot reviewed 56 out of 56 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/utils/focusController.ts | Formatting/type-safety-friendly signature formatting for focus utilities. |
| frontend/src/utils/focusController.test.ts | Updates tests to match typed/rewrapped callbacks. |
| frontend/src/services/uex.service.ts | Tightens service method signature formatting/types. |
| frontend/src/services/inventory.service.ts | Improves typing and readability of query building + service calls. |
| frontend/src/pages/ResetPassword.tsx | Minor typed error handling / formatting changes. |
| frontend/src/pages/Register.tsx | Narrows caught error to unknown and formats conditional error message. |
| frontend/src/pages/Profile.tsx | Re-formats state and UI props; minor typing-related cleanup. |
| frontend/src/pages/Login.tsx | Narrows caught error to unknown; formats error message selection. |
| frontend/src/pages/Inventory.tsx | Large typing/formatting pass; stronger generic usage for focus controller, state typing. |
| frontend/src/pages/Inventory.editor-mode.test.tsx | Updates mocks/tests for typed signatures. |
| frontend/src/pages/Home.tsx | Formatting-only changes. |
| frontend/src/pages/Dashboard.tsx | Formatting-only changes. |
| frontend/src/index.tsx | Minor syntax/quote consistency updates. |
| frontend/src/components/location/SystemLocationSelector.tsx | Tightens typing for hierarchyPath casting; formatting. |
| frontend/src/components/inventory/InventoryPortlet.tsx | Minor formatting; typed event handler signature formatting. |
| frontend/src/components/inventory/InventoryNewRow.tsx | Refines prop typing for draft and related expressions. |
| frontend/src/components/inventory/InventoryInlineRow.tsx | Tightens prop typing; formatting and minor type-safe refactors. |
| frontend/src/components/inventory/InventoryFiltersPanel.tsx | Tightens select values/casts and formatting for typed handlers. |
| frontend/.eslintrc.cjs | Enforces @typescript-eslint/no-explicit-any on the frontend. |
| docs/matrix-academy-comparison.md | Markdown formatting/table fixes and code sample punctuation. |
| backend/src/modules/users/users.controller.ts | Uses AuthenticatedRequest instead of any for req.user. |
| backend/src/modules/user-inventory/user-inventory.controller.ts | Replaces any request/query types with typed request + narrowed query shape. |
| backend/src/modules/user-inventory/entities/inventory-audit-log.entity.ts | Changes metadata typing to Record<string, unknown>. |
| backend/src/modules/uex/repositories/base-uex.repository.ts | Replaces as any casts with FindOptionsWhere<T> + typed update DTOs. |
| backend/src/modules/uex-sync/uex-sync.service.ts | Narrows any to unknown (but still uses a null-cast workaround in query). |
| backend/src/modules/uex-sync/uex-sync.controller.ts | Narrows caught errors to unknown and uses instanceof Error for messaging. |
| backend/src/modules/uex-sync/services/locations-sync.service.ts | Removes any from errors/filters; introduces unknown + narrowing/casts. |
| backend/src/modules/uex-sync/services/items-sync.service.ts | Removes any from errors/filters; introduces unknown + narrowing/casts. |
| backend/src/modules/uex-sync/services/companies-sync.service.ts | Removes any from errors; introduces unknown + narrowing/casts. |
| backend/src/modules/uex-sync/services/categories-sync.service.ts | Removes any from errors/filters; introduces unknown + narrowing/casts. |
| backend/src/modules/uex-sync/schedulers/uex-sync.scheduler.ts | Narrows scheduler errors to unknown and logs safely. |
| backend/src/modules/uex-sync/clients/uex-locations.client.ts | Converts any error handling to unknown + safer status/message extraction. |
| backend/src/modules/uex-sync/clients/uex-items.client.ts | Converts any error handling to unknown + safer status/message extraction. |
| backend/src/modules/uex-sync/clients/uex-companies.client.ts | Converts any error handling to unknown + safer status/message extraction. |
| backend/src/modules/uex-sync/clients/uex-categories.client.ts | Converts any error handling to unknown + safer status/message extraction. |
| backend/src/modules/org-inventory/org-inventory.controller.ts | Replaces any request/query types with typed request + narrowed query shape. |
| backend/src/modules/org-inventory/org-inventory.controller.spec.ts | Updates controller tests to use AuthenticatedRequest typing. |
| backend/src/modules/auth/token-cleanup.service.ts | Adds a new scheduled cleanup service for refresh tokens/password resets. |
| backend/src/modules/auth/jwt.strategy.ts | Adds typed JwtPayload and typed validate return (AuthenticatedUser). |
| backend/src/modules/auth/interfaces/validated-user.interface.ts | Introduces a typed ValidatedUser for local auth/login flows. |
| backend/src/modules/auth/interfaces/refresh-token-request.interface.ts | Introduces typed request/user shape for refresh-token guard flows. |
| backend/src/modules/auth/interfaces/jwt-payload.interface.ts | Introduces typed JWT payload interface used by strategy/service. |
| backend/src/modules/auth/interfaces/authenticated-request.interface.ts | Introduces typed authenticated request/user interface for controllers. |
| backend/src/modules/auth/auth.service.ts | Replaces any with `ValidatedUser |
| backend/src/modules/auth/auth.module.ts | Registers the new token cleanup provider. |
| backend/src/modules/auth/auth.controller.ts | Replaces any request types with typed request interfaces. |
| backend/src/modules/auth/auth.controller.spec.ts | Updates tests to use typed authenticated request objects. |
| backend/src/modules/audit-logs/audit-logs.service.ts | Changes metadata/values typing to Record<string, unknown>. |
| backend/src/modules/audit-logs/audit-log.entity.ts | Changes jsonb columns typing to Record<string, unknown>. |
| backend/src/common/interceptors/audit-log.interceptor.ts | Replaces any response/request usage with typed unknown + narrowing. |
| backend/src/common/filters/http-exception.filter.ts | Replaces any casts with a typed response interface + narrowing. |
| backend/src/app.module.ts | Narrows Redis connection error to unknown + safe message extraction. |
| backend/eslint.config.js | Flat-config formatting changes; still disables no-explicit-any. |
| backend/data-source.js | Formatting/quote normalization. |
| backend/.eslintrc.js | Enables @typescript-eslint/no-explicit-any: 'error' in legacy config. |
| backend/.env.example | Documents REFRESH_TOKEN_CLEANUP_CRON env var. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Enable @typescript-eslint/no-explicit-any: error in eslint.config.js (was off, conflicting with .eslintrc.js) - Normalize caught errors before recordSyncFailure() in all four sync services (error instanceof Error ? error : new Error(String(error))) to avoid silent loss of context when thrown value isn't an Error - Replace null as unknown as Date with IsNull() in uex-sync.service.ts for type-safe null comparison in TypeORM where clause - Remove token-cleanup.service.ts and its auth.module.ts registration from this branch (belongs in PR #114, not here) - Remove REFRESH_TOKEN_CLEANUP_CRON from .env.example (same reason)
Resolved conflicts in auth module by accepting main's httpOnly cookie implementation (login/refresh/logout now set cookies via @res) while preserving typed interfaces (JwtPayload, AuthenticatedUser, ValidatedUser, RefreshTokenRequest) in place of any casts introduced by main. Also fixed any violations in files added by main: - throttler.guard.ts: eslint-disable for required base-class override signature - auth.service.spec.ts: typed mock capture variables instead of any - auth.controller.ts: removed unused User import
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 56 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
audit-log.interceptor.ts: guard parseInt to only parse pure-digit strings,
preventing silent truncation of non-numeric IDs like UUIDs that start with
digits (e.g. "1e3..." would have become 1).
users.controller.ts: switch getProfile from Express Request to
AuthenticatedRequest so it returns a stable { id, username } shape
consistent with the rest of the auth-aware endpoints.
- Replace let mock*: any with Record<string, jest.Mock> - Replace defaultValue: any with defaultValue: unknown in ConfigService mocks - Replace async (callback: any) with typed callback signatures - Replace as any casts with as unknown as ConcreteType in spyOn mocks - Replace savedItems: any[] with Record<string, unknown>[] - Type repository variable in base-uex.repository.spec.ts using BaseUexRepository<BaseUexEntity>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 65 out of 65 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ation
- Use explicit interface type for mockCategoryRepository and mockItemRepository
to allow nested manager.transaction without conflicting with Record<string, jest.Mock>
- Replace Parameters<typeof repository.findOneActive>[0] cast with
FindOptionsWhere<BaseUexEntity> for correct where-clause typing
- Fix mockResolvedValue({}) on update spy to pass UpdateResult shape
- throttler.guard: change getTracker param from Record<string, any> to Record<string, unknown>, removing the eslint-disable comment - Extract shared QueryParams type to common/types/query-params.type.ts and replace duplicate local interface in org-inventory and user-inventory controllers
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 66 out of 66 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- base-uex.repository: inline update payloads directly into this.update() calls, removing the intermediate named variables and making the single cast site obvious - audit-log.interceptor: remove async from tap() callback and add .catch() so auditLogsService.log() failures are silently swallowed rather than surfacing as unhandled promise rejections - docs/matrix-academy-comparison.md: replace exceptionResponse as any with Record<string, unknown> narrowing; change Observable<any> to Observable<unknown> in interceptor example
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 66 out of 66 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Returning { id: req.user.userId } was a breaking change — existing clients
read the userId field. Reverted to { userId, username } to match the
AuthenticatedUser shape and avoid breaking consumers.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 66 out of 66 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- users.controller: destructure password out of updateProfile response so the hash is never sent to the client - query-params.type: widen QueryParams to match Express ParsedQs (string | string[] | QueryParams | QueryParams[] | undefined) and add asString() helper to narrow safely; update both inventory controllers to use asString() for search/sort/order fields
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 66 out of 66 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- auth/me: return userId field (was id) to match frontend expectations and ValidatedUser interface contract - audit-log interceptor: guard newValues assignment behind isPlainObject check so arrays/primitives don't get cast as AuditLogResponse
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 66 out of 66 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
backend/src/modules/user-inventory/user-inventory.controller.ts:175
getAuditLog()declares@Query('userId') userId?: number/orgId?: number/limit?: number, but without pipes these values arrive as strings at runtime. The currentNumber(userId)/Number(orgId)/Number(limit)conversions will produceNaNfor invalid input and pass it downstream. Prefer typing these query params asstring | undefined(or usingParseIntPipe/DefaultValuePipe) and validating so invalid values fail fast with a 400.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Introduces a BaseUexUpdate type alias (QueryDeepPartialEntity<BaseUexEntity>) so update payloads are typed against the known base fields before being narrowed to QueryDeepPartialEntity<T>. Removes the as unknown as double-cast that the reviewer flagged as obscuring real type mismatches.
Replaces bare Number() coercions with a readOptionalNumber() helper (matching the pattern in OrgInventoryController) that throws BadRequestException on non-numeric or non-integer inputs. Prevents NaN propagation into pagination and DB filter logic.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 66 out of 66 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
anytypes with proper TypeScript types across backend and frontend@typescript-eslint/no-explicit-anyvia ESLint in both packagesChanges
Backend:
AuthenticatedRequesttype replacinganyin controllerserror instanceof Error)Record<string, unknown>for audit log metadataFrontend:
focusControllertypedTest plan
pnpm typecheckpasses with zero errorspnpm lintpasses with nono-explicit-anyviolationspnpm testpassesCloses #100