Skip to content

Feature/interview portal#25

Open
shivams10 wants to merge 3 commits into
mainfrom
feature/interview-portal
Open

Feature/interview portal#25
shivams10 wants to merge 3 commits into
mainfrom
feature/interview-portal

Conversation

@shivams10

@shivams10 shivams10 commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Description:

feat: bootstrap interview portal with auth, schema, and app shell

Phases 1–2: extends Prisma schema with 8 new interview portal tables and
recruiter/interviewer roles; hardens auth with split JWT storage across
localStorage and server-set cookies; adds role-based layouts with sidebar
navigation, shared UI components, and a fully wired route structure.

Summary by CodeRabbit

  • New Features

    • Launched interview portal platform with dedicated dashboards for recruiters and interviewers.
    • Added role-based access control with new recruiter and interviewer user roles.
    • Implemented secure split-token authentication mechanism for improved security.
    • Added user profile endpoint for account information retrieval.
  • Chores

    • Extended database schema to support interview scheduling, candidate management, and hiring workflows.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces an interview portal feature across backend and a new frontend app. The backend adds a Prisma schema migration with 8 new interview-domain models and enums, extends UserRole, and rewrites the auth flow to use a split-token (body in localStorage, signature in cookie) mechanism with a new /auth/me endpoint. A new apps/interview-portal-fe Vite+React app is scaffolded with token utilities, an Axios client with auto-refresh, React Router routing, auth guards, login/callback pages, icons, UI components, role-aware layouts, and stub portal pages.

Changes

Backend: Interview Portal Data Model & Auth

Layer / File(s) Summary
Interview portal schema, enums, and migration
apps/backend/package.json, apps/backend/prisma/schema.prisma, apps/backend/prisma/migrations/.../migration.sql
Adds recruiter/interviewer to UserRole, defines 6 new interview portal enums, extends User with invite-chain fields, and adds 8 new models (Candidate, MeetingRoom, InterviewSession, SessionQuestion, SessionLink, SessionNote, SessionFeedback, HiringDecision) with indexes and FK constraints. Matching SQL migration and Prisma seed script config are included.
Split-token auth flow and /auth/me endpoint
apps/backend/src/modules/auth/strategies/jwt.strategy.ts, apps/backend/src/modules/auth/auth.service.ts, apps/backend/src/modules/auth/auth.controller.ts
Extends JwtPayload.role union type, adds AuthService.getProfile, rewrites the refresh handler to support an Authorization-header split-token path, adds GET /auth/me guarded by JwtAuthGuard, and changes the Google OAuth callback to emit token body halves as query params and signature halves as cookies.

Frontend: Interview Portal React App

Layer / File(s) Summary
Project scaffold: Vite, TypeScript, Tailwind, ESLint
apps/interview-portal-fe/package.json, apps/interview-portal-fe/index.html, apps/interview-portal-fe/vite.config.ts, apps/interview-portal-fe/tsconfig*.json, apps/interview-portal-fe/eslint.config.js, apps/interview-portal-fe/postcss.config.js, apps/interview-portal-fe/tailwind.config.ts, apps/interview-portal-fe/src/styles/globals.css, apps/interview-portal-fe/src/vite-env.d.ts
Creates the full project configuration: npm scripts/deps, Vite with @ alias, TypeScript strict config, ESLint flat config with React/TS strict rules, PostCSS/Tailwind CSS-variable design token system, and a global stylesheet with base resets.
Split-token storage, cookie util, Axios client, React Query
apps/interview-portal-fe/src/lib/token.ts, apps/interview-portal-fe/src/lib/cookie.ts, apps/interview-portal-fe/src/lib/utils.ts, apps/interview-portal-fe/src/lib/api.ts, apps/interview-portal-fe/src/lib/react-query.tsx, apps/interview-portal-fe/src/constants/env.ts
Implements split-token storage (localStorage body + cookie signature), isAuthenticated/decodeToken helpers, a cookieStorage util, cn class-merge utility, an Axios client with request interceptor for bearer tokens, a response interceptor for 401 auto-refresh with queued retry, and a shared QueryClient with ReactQueryProvider.
Routes, auth guards, login page, and auth callback
apps/interview-portal-fe/src/constants/routes.ts, apps/interview-portal-fe/src/constants/errors.ts, apps/interview-portal-fe/src/components/auth/ProtectedRoute.tsx, apps/interview-portal-fe/src/components/auth/PublicRoute.tsx, apps/interview-portal-fe/src/pages/auth/AuthCallbackPage.tsx, apps/interview-portal-fe/src/pages/auth/LoginPage.tsx, apps/interview-portal-fe/src/pages/errors/NotFoundPage.tsx
Defines ROUTES/ROLE_REDIRECT/AUTH_ERRORS constants, ProtectedRoute and PublicRoute guards using token state, a two-panel LoginPage with Google SSO redirect, an AuthCallbackPage that stores split token bodies and redirects by role, and a NotFoundPage.
Icon library and UI component kit
apps/interview-portal-fe/src/components/icons/*, apps/interview-portal-fe/src/components/ui/*
Adds IconProps interface, 11 SVG icon components (AlertCircle, Calendar, ClipboardList, Google, Home, LogOut, Menu, PortalLogo, Sparkle, Users, X) with barrel export, and UI components Avatar, Badge, EmptyState, LoadingSpinner/PageLoader, PageHeader, Skeleton/SkeletonRow with barrel export.
AppLayout, role-specific layouts, and user hooks
apps/interview-portal-fe/src/hooks/useCurrentUser.ts, apps/interview-portal-fe/src/hooks/useLogout.ts, apps/interview-portal-fe/src/layouts/AppLayout.tsx, apps/interview-portal-fe/src/layouts/RecruiterLayout.tsx, apps/interview-portal-fe/src/layouts/InterviewerLayout.tsx
Adds useCurrentUser (React Query /auth/me), useLogout (token clear + cache reset), AppLayout (responsive sidebar with active NavLink, avatar, sign-out), RecruiterLayout and InterviewerLayout (role-based cross-redirect guards wrapping AppLayout).
Portal pages, router config, and app entrypoint
apps/interview-portal-fe/src/pages/recruiter/*, apps/interview-portal-fe/src/pages/interviewer/*, apps/interview-portal-fe/src/providers/AppProviders.tsx, apps/interview-portal-fe/src/routes/index.tsx, apps/interview-portal-fe/src/App.tsx, apps/interview-portal-fe/src/main.tsx
Adds stub portal pages for recruiter (Dashboard, Candidates, Schedule, Team) and interviewer (Dashboard, Sessions), defines the full createBrowserRouter tree with public/protected/layout-nested routes, and wires AppProvidersAppmain.tsx bootstrap.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant LoginPage
    participant Backend
    participant AuthCallbackPage
    participant tokenStorage
    participant api

    Browser->>LoginPage: visit /login
    LoginPage->>Browser: redirect to /auth/google
    Browser->>Backend: GET /auth/google → Google OAuth
    Backend-->>Browser: redirect /auth/callback?access=<body>&refresh=<body>
    Note over Browser: ip_access_sig + ip_refresh_sig cookies set
    Browser->>AuthCallbackPage: /auth/callback?access=...&refresh=...
    AuthCallbackPage->>tokenStorage: set(accessBody, refreshBody)
    AuthCallbackPage-->>Browser: navigate to role dashboard

    Browser->>api: authenticated request
    api->>Backend: with Authorization: Bearer <body.sig>
    Backend-->>api: 401 (expired)
    api->>Backend: POST /auth/refresh (Authorization: Bearer <refreshBody.sig>)
    Backend-->>api: { accessBody } + new ip_access_sig cookie
    api->>tokenStorage: setBody(accessBody)
    api->>Backend: retry original request
    Backend-->>Browser: 200 response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • shivams10/nest-assessment#22: Modifies the same auth.controller.ts Google OAuth callback and token/cookie handling logic that this PR also rewrites with the split-token approach.

Poem

🐇 A portal springs from fields of code,
Where interviewers find their abode.
Split tokens dance in cookie jars,
While recruiters schedule under stars.
The rabbit hops — new routes now gleam,
Phase one complete, a portal's dream! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Feature/interview portal" is vague and uses generic phrasing that doesn't clearly summarize the main changes; it lacks specificity about what aspect of the interview portal is being introduced. Consider revising the title to be more specific, such as "Introduce interview portal with new recruiter/interviewer roles and split JWT authentication" to better capture the main changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 feature/interview-portal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 14

🧹 Nitpick comments (2)
apps/interview-portal-fe/src/components/icons/types.ts (1)

4-4: 💤 Low value

Simplify aria-hidden type to boolean.

The union boolean | 'true' | 'false' is redundant; React's aria-hidden prop accepts boolean directly, and TypeScript's boolean type already covers true and false primitive values. The string literals 'true' | 'false' are unnecessary.

♻️ Proposed simplification
-  'aria-hidden'?: boolean | 'true' | 'false'
+  'aria-hidden'?: boolean
🤖 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 `@apps/interview-portal-fe/src/components/icons/types.ts` at line 4, The
aria-hidden property type in the types.ts file currently uses a redundant union
type of boolean | 'true' | 'false'. Simplify this to just boolean since React's
aria-hidden prop accepts boolean values directly, and the boolean type already
encompasses the true and false primitive values, making the string literal types
unnecessary. Replace the current aria-hidden type definition with a simple
boolean type.
apps/interview-portal-fe/src/pages/auth/AuthCallbackPage.tsx (1)

50-53: 💤 Low value

Minor: tokenStorage.clearAll() is redundant here.

Tokens haven't been stored yet at this point in the flow, so clearing them is a no-op. While defensive, this call can be removed for clarity.

♻️ Optional cleanup
     if (!destination) {
-      tokenStorage.clearAll()
       void navigate(`${ROUTES.LOGIN}?error=not_registered`, { replace: true })
       return
     }
🤖 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 `@apps/interview-portal-fe/src/pages/auth/AuthCallbackPage.tsx` around lines 50
- 53, The tokenStorage.clearAll() call in the destination check block within
AuthCallbackPage.tsx is unnecessary because no tokens have been stored at this
point in the authentication callback flow, making it a no-op. Remove the
tokenStorage.clearAll() line from the if (!destination) block to improve code
clarity while maintaining the same behavior of navigating to the login error
page.
🤖 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 @.claude/settings.json:
- Around line 9-10: The settings.json file contains hard-coded repo paths in the
git hook commands that only work for the specific absolute path
`/Users/gkmit/code/bb-sessioon/nest-assessment`. This breaks functionality in
other clones or environments. In both hook configurations (at lines 9-10 and
21-22), replace the hard-coded absolute repo path with a runtime-resolved repo
root path. Instead of using the fixed string path in the "command" field, use a
method or variable that dynamically determines the current repository root at
execution time so the hooks work correctly regardless of where the repository is
cloned.

In `@apps/backend/prisma/migrations/20260615040933_portal_phase1/migration.sql`:
- Around line 200-206: The migration creates a redundant index on
SessionLink.token field: a UNIQUE INDEX on line 203 provides index
functionality, making the additional regular INDEX on line 206 unnecessary and
wasteful. Remove the `@@index([token])` directive from the SessionLink model
definition in the Prisma schema file (typically schema.prisma) to eliminate this
redundancy, while keeping the unique constraint on the token field intact.

In `@apps/backend/src/modules/auth/auth.controller.ts`:
- Around line 83-94: The token splitting logic using parts[2] ?? '' masks
malformed JWT tokens by silently producing empty signatures instead of raising
errors. In apps/backend/src/modules/auth/auth.controller.ts at lines 83-94 in
the refresh() method, validate that accessToken.split('.') produces exactly 3
parts and throw an error if validation fails instead of using the empty string
fallback. Apply the same validation check for both access and refresh tokens in
apps/backend/src/modules/auth/auth.controller.ts at lines 186-191 in the
googleAuthCallback() method before extracting and using the signature parts.

In `@apps/backend/src/modules/auth/auth.service.ts`:
- Around line 245-259: The findUnique query in the user profile fetch does not
filter out soft-deleted users by checking for deletedAt, which allows
soft-deleted users with valid JWTs to access their profile. Add deletedAt: null
to the where clause of the findUnique query to ensure only non-deleted users can
retrieve their profile information, matching the pattern used in
refreshTokens().

In `@apps/interview-portal-fe/src/components/auth/PublicRoute.tsx`:
- Around line 10-11: The PublicRoute component creates an infinite redirect loop
by redirecting authenticated users with unmapped roles back to ROUTES.LOGIN.
Instead of using ROUTES.LOGIN as the default fallback destination in the
Navigate component on line 11, implement an alternative destination such as a
dedicated error or access-denied page, or clear the authentication tokens and
redirect to an auth-cleared state. Ensure the fallback destination properly
handles authenticated users with unknown or invalid roles without cycling them
back through the PublicRoute guard.

In `@apps/interview-portal-fe/src/components/icons/PortalLogoIcon.tsx`:
- Around line 5-12: The PortalLogoIcon component is missing the
aria-hidden="true" attribute on its SVG element, which creates an accessibility
inconsistency with other icon components in the library. Add aria-hidden="true"
to the SVG element in PortalLogoIcon to properly hide the decorative icon from
screen readers, ensuring it aligns with ARIA best practices and maintains
consistency with the rest of the icon library.

In `@apps/interview-portal-fe/src/components/ui/SkeletonRow.tsx`:
- Line 11: Replace the deprecated Tailwind CSS class `flex-shrink-0` with
`shrink-0` in the SkeletonRow.tsx Skeleton component. Additionally, search the
entire codebase for all occurrences of `flex-shrink-*` classes (including
flex-shrink-0 and flex-shrink variations) in the files mentioned: LoginPage.tsx,
AppLayout.tsx, PageHeader.tsx, Avatar.tsx, ExamsTable.tsx, and
ReadinessPanel.tsx, and replace each instance with the corresponding modern
Tailwind v4 equivalent (e.g., flex-shrink-0 becomes shrink-0, flex-shrink
becomes shrink). This aligns the codebase with Tailwind CSS v4 naming
conventions.

In `@apps/interview-portal-fe/src/hooks/useLogout.ts`:
- Around line 11-15: The useLogout hook currently only clears frontend state
(tokenStorage and queryClient) but does not invalidate server-set signature
cookies (ip_access_sig and ip_refresh_sig), which persist and allow session
continuation. Create a backend logout endpoint that expires these signature
cookies by setting maxAge: 0, then update the function returned by useLogout to
call this backend endpoint before executing tokenStorage.clearAll() and
queryClient.clear(). This ensures signature cookies are cleared server-side
during logout.

In `@apps/interview-portal-fe/src/layouts/InterviewerLayout.tsx`:
- Around line 17-22: The current check in InterviewerLayout.tsx uses a blocklist
approach by only blocking 'recruiter' and 'admin' roles, which allows any other
authenticated role to access the interviewer portal. Switch to an allowlist
approach by modifying the condition to explicitly verify that the payload role
is 'interviewer', and if it is not, redirect to an appropriate default route
(such as ROUTES.RECRUITER_DASHBOARD or a login route). This ensures only users
with the 'interviewer' role can render the AppLayout component in the
InterviewerLayout.

In `@apps/interview-portal-fe/src/layouts/RecruiterLayout.tsx`:
- Around line 19-24: The RecruiterLayout component uses a deny-list approach
that only blocks the 'interviewer' role, which allows unintended roles like
'candidate' and 'moderator' to access the recruiter portal. Replace the role
guard check with an allow-list pattern that explicitly permits only 'recruiter'
and 'admin' roles. The condition should check if the payload role exists and is
NOT included in the allowed roles list, redirecting to
ROUTES.INTERVIEWER_DASHBOARD if the check fails, matching the security pattern
already implemented in InterviewerLayout.

In `@apps/interview-portal-fe/src/lib/cookie.ts`:
- Around line 13-15: The get function in the cookie lookup constructs a RegExp
using the raw name parameter, which can fail if the cookie name contains regex
metacharacters. Replace the regex-based approach with a string split and
startsWith lookup instead: split document.cookie by semicolons and spaces, then
iterate through the resulting array to find a cookie entry that starts with the
desired name followed by an equals sign, extracting the value without relying on
regex construction.

In `@apps/interview-portal-fe/src/lib/token.ts`:
- Around line 14-20: The JwtPayload type definition includes email as a required
field, but the backend auth payload contract only guarantees sub and role (with
iat and exp). Make the email field optional in the JwtPayload type by changing
it to email?: string to align with the actual backend contract and prevent
undefined assumptions from leaking into role and layout flows.
- Around line 58-63: The decodeToken function uses atob() to decode the JWT
payload segment, but JWT payloads use base64url encoding (RFC 7518) which
replaces `+` with `-` and `/` with `_`, and omits padding. The atob() function
expects standard base64, so it fails on tokens with `-` or `_` characters. Fix
this by normalizing the base64url string before calling atob(): replace all `-`
characters with `+`, replace all `_` characters with `/`, add padding with `=`
characters as needed to make the length a multiple of 4, then pass the
normalized string to atob().

In `@apps/interview-portal-fe/vite.config.ts`:
- Around line 3-10: The `__dirname` variable used in the path.resolve call
within the alias configuration is undefined in ESM contexts, which will cause a
ReferenceError at runtime. To fix this, import `fileURLToPath` from the 'url'
module, then create an ESM-compatible `__dirname` replacement by combining
`fileURLToPath(import.meta.url)` with `path.dirname()`. Finally, replace the
existing `__dirname` reference in the alias configuration with this new
variable.

---

Nitpick comments:
In `@apps/interview-portal-fe/src/components/icons/types.ts`:
- Line 4: The aria-hidden property type in the types.ts file currently uses a
redundant union type of boolean | 'true' | 'false'. Simplify this to just
boolean since React's aria-hidden prop accepts boolean values directly, and the
boolean type already encompasses the true and false primitive values, making the
string literal types unnecessary. Replace the current aria-hidden type
definition with a simple boolean type.

In `@apps/interview-portal-fe/src/pages/auth/AuthCallbackPage.tsx`:
- Around line 50-53: The tokenStorage.clearAll() call in the destination check
block within AuthCallbackPage.tsx is unnecessary because no tokens have been
stored at this point in the authentication callback flow, making it a no-op.
Remove the tokenStorage.clearAll() line from the if (!destination) block to
improve code clarity while maintaining the same behavior of navigating to the
login error page.
🪄 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

Run ID: 10e38df0-7e9c-4ef5-902c-a4ff3f4ae297

📥 Commits

Reviewing files that changed from the base of the PR and between cc425ce and a2bf96e.

⛔ Files ignored due to path filters (2)
  • apps/interview-portal-fe/public/favicon.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (66)
  • .claude/settings.json
  • .gitignore
  • apps/backend/package.json
  • apps/backend/prisma/migrations/20260615040933_portal_phase1/migration.sql
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/modules/auth/auth.controller.ts
  • apps/backend/src/modules/auth/auth.service.ts
  • apps/backend/src/modules/auth/strategies/jwt.strategy.ts
  • apps/interview-portal-fe/eslint.config.js
  • apps/interview-portal-fe/index.html
  • apps/interview-portal-fe/package.json
  • apps/interview-portal-fe/postcss.config.js
  • apps/interview-portal-fe/src/App.tsx
  • apps/interview-portal-fe/src/components/auth/ProtectedRoute.tsx
  • apps/interview-portal-fe/src/components/auth/PublicRoute.tsx
  • apps/interview-portal-fe/src/components/icons/AlertCircleIcon.tsx
  • apps/interview-portal-fe/src/components/icons/CalendarIcon.tsx
  • apps/interview-portal-fe/src/components/icons/ClipboardListIcon.tsx
  • apps/interview-portal-fe/src/components/icons/GoogleIcon.tsx
  • apps/interview-portal-fe/src/components/icons/HomeIcon.tsx
  • apps/interview-portal-fe/src/components/icons/LogOutIcon.tsx
  • apps/interview-portal-fe/src/components/icons/MenuIcon.tsx
  • apps/interview-portal-fe/src/components/icons/PortalLogoIcon.tsx
  • apps/interview-portal-fe/src/components/icons/SparkleIcon.tsx
  • apps/interview-portal-fe/src/components/icons/UsersIcon.tsx
  • apps/interview-portal-fe/src/components/icons/XIcon.tsx
  • apps/interview-portal-fe/src/components/icons/index.ts
  • apps/interview-portal-fe/src/components/icons/types.ts
  • apps/interview-portal-fe/src/components/ui/Avatar.tsx
  • apps/interview-portal-fe/src/components/ui/Badge.tsx
  • apps/interview-portal-fe/src/components/ui/EmptyState.tsx
  • apps/interview-portal-fe/src/components/ui/LoadingSpinner.tsx
  • apps/interview-portal-fe/src/components/ui/PageHeader.tsx
  • apps/interview-portal-fe/src/components/ui/SkeletonRow.tsx
  • apps/interview-portal-fe/src/components/ui/index.ts
  • apps/interview-portal-fe/src/constants/env.ts
  • apps/interview-portal-fe/src/constants/errors.ts
  • apps/interview-portal-fe/src/constants/routes.ts
  • apps/interview-portal-fe/src/hooks/useCurrentUser.ts
  • apps/interview-portal-fe/src/hooks/useLogout.ts
  • apps/interview-portal-fe/src/layouts/AppLayout.tsx
  • apps/interview-portal-fe/src/layouts/InterviewerLayout.tsx
  • apps/interview-portal-fe/src/layouts/RecruiterLayout.tsx
  • apps/interview-portal-fe/src/lib/api.ts
  • apps/interview-portal-fe/src/lib/cookie.ts
  • apps/interview-portal-fe/src/lib/react-query.tsx
  • apps/interview-portal-fe/src/lib/token.ts
  • apps/interview-portal-fe/src/lib/utils.ts
  • apps/interview-portal-fe/src/main.tsx
  • apps/interview-portal-fe/src/pages/auth/AuthCallbackPage.tsx
  • apps/interview-portal-fe/src/pages/auth/LoginPage.tsx
  • apps/interview-portal-fe/src/pages/errors/NotFoundPage.tsx
  • apps/interview-portal-fe/src/pages/interviewer/InterviewerDashboardPage.tsx
  • apps/interview-portal-fe/src/pages/interviewer/InterviewerSessionsPage.tsx
  • apps/interview-portal-fe/src/pages/recruiter/RecruiterCandidatesPage.tsx
  • apps/interview-portal-fe/src/pages/recruiter/RecruiterDashboardPage.tsx
  • apps/interview-portal-fe/src/pages/recruiter/RecruiterSchedulePage.tsx
  • apps/interview-portal-fe/src/pages/recruiter/RecruiterTeamPage.tsx
  • apps/interview-portal-fe/src/providers/AppProviders.tsx
  • apps/interview-portal-fe/src/routes/index.tsx
  • apps/interview-portal-fe/src/styles/globals.css
  • apps/interview-portal-fe/src/vite-env.d.ts
  • apps/interview-portal-fe/tailwind.config.ts
  • apps/interview-portal-fe/tsconfig.app.json
  • apps/interview-portal-fe/tsconfig.json
  • apps/interview-portal-fe/vite.config.ts

Comment thread .claude/settings.json
Comment on lines +9 to +10
"command": "cat >/dev/null || true; git rev-parse --git-dir >/dev/null 2>&1 && code-review-graph update --skip-flows --repo \"/Users/gkmit/code/bb-sessioon/nest-assessment\" || true",
"timeout": 30

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 | 🟠 Major | ⚡ Quick win

Remove the hard-coded repo path from both hooks.

These commands only work for /Users/gkmit/code/bb-sessioon/nest-assessment; in any other clone they’ll target the wrong repo or no-op. Resolve the repo root at runtime instead.

Suggested fix
-        "command": "cat >/dev/null || true; git rev-parse --git-dir >/dev/null 2>&1 && code-review-graph update --skip-flows --repo \"/Users/gkmit/code/bb-sessioon/nest-assessment\" || true",
+        "command": "repo_root=\"$(git rev-parse --show-toplevel)\" && code-review-graph update --skip-flows --repo \"$repo_root\" || true",
...
-        "command": "cat >/dev/null || true; git rev-parse --git-dir >/dev/null 2>&1 && code-review-graph status --repo \"/Users/gkmit/code/bb-sessioon/nest-assessment\" || echo 'Not a git repo, skipping'",
+        "command": "repo_root=\"$(git rev-parse --show-toplevel)\" && code-review-graph status --repo \"$repo_root\" || echo 'Not a git repo, skipping'",

Also applies to: 21-22

🤖 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 @.claude/settings.json around lines 9 - 10, The settings.json file contains
hard-coded repo paths in the git hook commands that only work for the specific
absolute path `/Users/gkmit/code/bb-sessioon/nest-assessment`. This breaks
functionality in other clones or environments. In both hook configurations (at
lines 9-10 and 21-22), replace the hard-coded absolute repo path with a
runtime-resolved repo root path. Instead of using the fixed string path in the
"command" field, use a method or variable that dynamically determines the
current repository root at execution time so the hooks work correctly regardless
of where the repository is cloned.

Comment on lines +200 to +206
CREATE UNIQUE INDEX "SessionLink_sessionId_key" ON "SessionLink"("sessionId");

-- CreateIndex
CREATE UNIQUE INDEX "SessionLink_token_key" ON "SessionLink"("token");

-- CreateIndex
CREATE INDEX "SessionLink_token_idx" ON "SessionLink"("token");

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

Redundant index on SessionLink.token.

Line 203 creates a unique index on token, which already provides index functionality. The additional index on line 206 is redundant and wastes storage space.

This should be removed from the schema's @@index([token]) directive on SessionLink.

🤖 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 `@apps/backend/prisma/migrations/20260615040933_portal_phase1/migration.sql`
around lines 200 - 206, The migration creates a redundant index on
SessionLink.token field: a UNIQUE INDEX on line 203 provides index
functionality, making the additional regular INDEX on line 206 unnecessary and
wasteful. Remove the `@@index([token])` directive from the SessionLink model
definition in the Prisma schema file (typically schema.prisma) to eliminate this
redundancy, while keeping the unique constraint on the token field intact.

Comment on lines +83 to +94
const parts = accessToken.split('.');
const accessBody = `${parts[0]}.${parts[1]}`;
const accessSig = parts[2] ?? '';

res.cookie('ip_access_sig', accessSig, {
httpOnly: false,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 30 * 60 * 1000,
});

return { accessBody };

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

Empty signature fallback in token splitting may mask invalid tokens. Both refresh() and googleAuthCallback() use parts[2] ?? '' when splitting JWTs, which produces empty signatures if tokens are malformed. This leads to invalid cookies and silent auth failures rather than explicit errors.

  • apps/backend/src/modules/auth/auth.controller.ts#L83-L94: Validate that accessToken.split('.') produces exactly 3 parts before using them in refresh().
  • apps/backend/src/modules/auth/auth.controller.ts#L186-L191: Apply the same validation for both access and refresh tokens in googleAuthCallback().
📍 Affects 1 file
  • apps/backend/src/modules/auth/auth.controller.ts#L83-L94 (this comment)
  • apps/backend/src/modules/auth/auth.controller.ts#L186-L191
🤖 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 `@apps/backend/src/modules/auth/auth.controller.ts` around lines 83 - 94, The
token splitting logic using parts[2] ?? '' masks malformed JWT tokens by
silently producing empty signatures instead of raising errors. In
apps/backend/src/modules/auth/auth.controller.ts at lines 83-94 in the refresh()
method, validate that accessToken.split('.') produces exactly 3 parts and throw
an error if validation fails instead of using the empty string fallback. Apply
the same validation check for both access and refresh tokens in
apps/backend/src/modules/auth/auth.controller.ts at lines 186-191 in the
googleAuthCallback() method before extracting and using the signature parts.

Comment on lines +245 to +259
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
role: true,
isActive: true,
},
});
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
return user;

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 | 🟠 Major | ⚡ Quick win

Missing deletedAt check allows soft-deleted users to access profile.

The query doesn't filter out soft-deleted users, unlike refreshTokens() which includes deletedAt: null. A soft-deleted user with a valid JWT (issued before deletion) could still access their profile.

🛠️ Suggested fix
     const user = await this.prisma.user.findUnique({
-      where: { id: userId },
+      where: { id: userId, deletedAt: null },
       select: {
         id: true,
         email: true,
📝 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
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
role: true,
isActive: true,
},
});
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
return user;
const user = await this.prisma.user.findUnique({
where: { id: userId, deletedAt: null },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
role: true,
isActive: true,
},
});
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
return user;
🤖 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 `@apps/backend/src/modules/auth/auth.service.ts` around lines 245 - 259, The
findUnique query in the user profile fetch does not filter out soft-deleted
users by checking for deletedAt, which allows soft-deleted users with valid JWTs
to access their profile. Add deletedAt: null to the where clause of the
findUnique query to ensure only non-deleted users can retrieve their profile
information, matching the pattern used in refreshTokens().

Comment on lines +10 to +11
const dest = payload?.role ? (ROLE_REDIRECT[payload.role] ?? null) : null
return <Navigate to={dest ?? ROUTES.LOGIN} replace />

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 | 🔴 Critical | ⚡ Quick win

Redirect loop: authenticated users with unknown roles will cycle back to LOGIN.

When an authenticated user has a role not in ROLE_REDIRECT (or a null/malformed token), Line 11 redirects to ROUTES.LOGIN. Since PublicRoute guards the login page, this creates an infinite redirect loop: LOGIN → PublicRoute detects auth → redirects to LOGIN again.

Consider redirecting to a dedicated error page, a default dashboard, or clearing tokens and showing an access-denied message.

🔄 Proposed fix to prevent redirect loop
-    const dest    = payload?.role ? (ROLE_REDIRECT[payload.role] ?? null) : null
-    return <Navigate to={dest ?? ROUTES.LOGIN} replace />
+    const dest = payload?.role ? ROLE_REDIRECT[payload.role] : null
+    if (!dest) {
+      // Clear invalid tokens and redirect to login with error
+      accessTokenStore.clear()
+      return <Navigate to={`${ROUTES.LOGIN}?error=not_registered`} replace />
+    }
+    return <Navigate to={dest} replace />
🤖 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 `@apps/interview-portal-fe/src/components/auth/PublicRoute.tsx` around lines 10
- 11, The PublicRoute component creates an infinite redirect loop by redirecting
authenticated users with unmapped roles back to ROUTES.LOGIN. Instead of using
ROUTES.LOGIN as the default fallback destination in the Navigate component on
line 11, implement an alternative destination such as a dedicated error or
access-denied page, or clear the authentication tokens and redirect to an
auth-cleared state. Ensure the fallback destination properly handles
authenticated users with unknown or invalid roles without cycling them back
through the PublicRoute guard.

Comment on lines +19 to +24
// Interviewer who manually navigates here → send to their portal
if (payload?.role === 'interviewer') {
return <Navigate to={ROUTES.INTERVIEWER_DASHBOARD} replace />
}

return <AppLayout navItems={NAV_ITEMS} portalTitle="Recruitment" />

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Backend role enum =="
rg -n -C4 "enum UserRole|recruiter|interviewer|admin" apps/backend/prisma/schema.prisma

echo
echo "== Frontend role guards =="
sed -n '1,220p' apps/interview-portal-fe/src/layouts/RecruiterLayout.tsx
sed -n '1,220p' apps/interview-portal-fe/src/layouts/InterviewerLayout.tsx
sed -n '1,260p' apps/interview-portal-fe/src/components/auth/ProtectedRoute.tsx

Repository: shivams10/nest-assessment

Length of output: 4606


Change RecruiterLayout guard to allow-list for consistency and security.

Line 20 uses a deny-list approach (blocks only interviewer), allowing candidate, moderator, and other unintended roles to access the recruiter portal. Update to match the allow-list pattern used in InterviewerLayout:

Suggested fix
  if (payload?.role && !['recruiter', 'admin'].includes(payload.role)) {
    return <Navigate to={ROUTES.INTERVIEWER_DASHBOARD} replace />
  }

InterviewerLayout already implements this correctly with explicit allow-list logic for its intended roles.

🤖 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 `@apps/interview-portal-fe/src/layouts/RecruiterLayout.tsx` around lines 19 -
24, The RecruiterLayout component uses a deny-list approach that only blocks the
'interviewer' role, which allows unintended roles like 'candidate' and
'moderator' to access the recruiter portal. Replace the role guard check with an
allow-list pattern that explicitly permits only 'recruiter' and 'admin' roles.
The condition should check if the payload role exists and is NOT included in the
allowed roles list, redirecting to ROUTES.INTERVIEWER_DASHBOARD if the check
fails, matching the security pattern already implemented in InterviewerLayout.

Comment on lines +13 to +15
get: (name: string): string | null => {
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
if (!match || !match[1]) return null

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

Avoid regex construction from raw cookie name.

Line 14 interpolates name directly into new RegExp(...); cookie names with regex metacharacters can cause incorrect matches or parse failures. Prefer a split/startsWith lookup.

Suggested deterministic cookie lookup
   get: (name: string): string | null => {
-    const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
-    if (!match || !match[1]) return null
-    return decodeURIComponent(match[1])
+    const prefix = `${name}=`
+    const found = document.cookie
+      .split('; ')
+      .find((entry) => entry.startsWith(prefix))
+    if (!found) return null
+    return decodeURIComponent(found.slice(prefix.length))
   },
📝 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
get: (name: string): string | null => {
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))
if (!match || !match[1]) return null
get: (name: string): string | null => {
const prefix = `${name}=`
const found = document.cookie
.split('; ')
.find((entry) => entry.startsWith(prefix))
if (!found) return null
return decodeURIComponent(found.slice(prefix.length))
},
🧰 Tools
🪛 ast-grep (0.43.0)

[warning] 13-13: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((?:^|; )${name}=([^;]*))
Note: [CWE-1333] Inefficient Regular Expression Complexity

(regexp-from-variable)

🤖 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 `@apps/interview-portal-fe/src/lib/cookie.ts` around lines 13 - 15, The get
function in the cookie lookup constructs a RegExp using the raw name parameter,
which can fail if the cookie name contains regex metacharacters. Replace the
regex-based approach with a string split and startsWith lookup instead: split
document.cookie by semicolons and spaces, then iterate through the resulting
array to find a cookie entry that starts with the desired name followed by an
equals sign, extracting the value without relying on regex construction.

Source: Linters/SAST tools

Comment on lines +14 to +20
export type JwtPayload = {
sub: string
email: string
role: string
iat: number
exp: number
}

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 | 🟠 Major | ⚡ Quick win

Align JwtPayload with the backend claim contract.

Line 16 makes email required, but the backend auth payload contract is sub + role (with standard iat/exp). This creates a false type guarantee that can leak undefined assumptions into role/layout flows.

Suggested contract-safe type update
 export type JwtPayload = {
   sub:   string
-  email: string
   role:  string
   iat:   number
   exp:   number
+  email?: string
 }
📝 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
export type JwtPayload = {
sub: string
email: string
role: string
iat: number
exp: number
}
export type JwtPayload = {
sub: string
role: string
iat: number
exp: number
email?: 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 `@apps/interview-portal-fe/src/lib/token.ts` around lines 14 - 20, The
JwtPayload type definition includes email as a required field, but the backend
auth payload contract only guarantees sub and role (with iat and exp). Make the
email field optional in the JwtPayload type by changing it to email?: string to
align with the actual backend contract and prevent undefined assumptions from
leaking into role and layout flows.

Comment on lines +58 to +63
export const decodeToken = (token: string): JwtPayload | null => {
try {
const part = token.split('.')[1]
if (!part) return null
return JSON.parse(atob(part)) as JwtPayload
} catch {

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import base64, json
payload = {"sub":"1","role":"recruiter","iat":1700000000,"exp":1800000000}
part = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=")
print("JWT payload segment (base64url, unpadded):", part)

try:
    base64.b64decode(part, validate=True)
    print("raw base64 decode: unexpectedly succeeded")
except Exception as e:
    print("raw base64 decode fails as expected:", e)

padded = part + "=" * ((4 - len(part) % 4) % 4)
print("base64url normalized decode:", base64.urlsafe_b64decode(padded).decode())
PY

Repository: shivams10/nest-assessment

Length of output: 364


🏁 Script executed:

cat -n apps/interview-portal-fe/src/lib/token.ts | head -70

Repository: shivams10/nest-assessment

Length of output: 2992


🌐 Web query:

JavaScript atob() base64url unpadded JWT token decoding behavior

💡 Result:

The JavaScript atob function is designed to decode standard Base64, not the Base64URL encoding used in JWT tokens [1][2][3]. If you pass a raw JWT segment to atob, it will typically throw an "InvalidCharacterError" because Base64URL uses characters ('-' and '') that are not part of the standard Base64 alphabet ('+' and '/') [1][4][5]. Additionally, JWT segments often omit the '=' padding characters required by standard Base64 [4][2][3]. To correctly decode a Base64URL-encoded JWT segment in a browser environment using atob, you must perform the following transformations [1][2][5]: 1. Character Replacement: Replace '-' with '+' and '' with '/' [1][4][3]. 2. Padding Restoration: Add '=' characters to the end of the string until its length is a multiple of 4 [4][2][6]. Example Implementation: function decodeBase64Url(segment) { let base64 = segment.replace(/-/g, '+').replace(/_/g, '/'); // Add padding if missing const pad = base64.length % 4; if (pad === 2) base64 += '=='; else if (pad === 3) base64 += '='; // Decode using atob and convert from binary string to UTF-8 const binary = atob(base64); const bytes = Uint8Array.from(binary, c => c.charCodeAt(0)); return new TextDecoder.decode(bytes); } In modern JavaScript environments, including Node.js and newer browser versions, you have better alternatives: - Node.js: Use Buffer.from(segment, 'base64url').toString('utf8') [2][6][3]. - Modern Browsers: The newer Uint8Array.fromBase64(segment, { alphabet: 'base64url' }) API natively handles Base64URL without manual replacement or padding [5][7]. Important: Decoding a JWT only reveals its contents; it does not verify the token's signature [2][8]. Always use a verified JWT library for production authentication workflows [2][3].

Citations:


🏁 Script executed:

# Search for backend OAuth/JWT token generation code
find . -type f \( -name "*.ts" -o -name "*.js" \) -path "*/src/*" | xargs grep -l "OAuth\|JWT\|jwt\|token" | head -20

Repository: shivams10/nest-assessment

Length of output: 1198


🏁 Script executed:

# Look for test files that might show token format
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" \) | xargs grep -l "decodeToken\|base64" 2>/dev/null

Repository: shivams10/nest-assessment

Length of output: 53


🏁 Script executed:

# Search for any usages of the decodeToken function
rg "decodeToken" --type ts --type js -A 2 -B 2

Repository: shivams10/nest-assessment

Length of output: 4884


🏁 Script executed:

# Check backend JWT/auth strategy to see how tokens are generated
cat -n apps/backend/src/modules/auth/strategies/jwt.strategy.ts

Repository: shivams10/nest-assessment

Length of output: 1100


🏁 Script executed:

# Look for JWT library/passport configuration
rg "JwtModule\|sign\|secret" apps/backend/src/modules/auth/ --type ts -A 2 -B 2

Repository: shivams10/nest-assessment

Length of output: 51


🏁 Script executed:

# Check if there are any actual token examples in tests or config
find . -type f \( -name "*.ts" -o -name "*.json" \) | xargs grep -l "eyJ" 2>/dev/null | head -10

Repository: shivams10/nest-assessment

Length of output: 51


🏁 Script executed:

# Search for how tokens are signed on the backend (likely jsonwebtoken or similar)
rg "sign|jsonwebtoken|jwt" apps/backend/src/modules/auth/ --type ts | head -20

Repository: shivams10/nest-assessment

Length of output: 2226


🏁 Script executed:

# Check auth controller to see how tokens are generated
cat -n apps/backend/src/modules/auth/auth.controller.ts | head -100

Repository: shivams10/nest-assessment

Length of output: 3964


🏁 Script executed:

# Let's verify the actual JWT structure generated by NestJS JWT
# by checking if there's any test or documentation about token format
cat -n apps/backend/src/modules/auth/auth.service.ts | head -120

Repository: shivams10/nest-assessment

Length of output: 4152


🏁 Script executed:

# Check if there are any unit tests that show actual JWT tokens or their structure
find . -type f -name "*.spec.ts" | xargs grep -l "accessToken\|refreshToken\|jwt" 2>/dev/null | head -5

Repository: shivams10/nest-assessment

Length of output: 51


Fix JWT decoding to handle base64url encoding.

Line 62 uses atob(part) directly on JWT payload segments, which are base64url-encoded per RFC 7518. Since atob() only handles standard base64 (with +// and padding), it will fail on unpadded base64url tokens containing - or _ characters, causing authentication and expiry checks to break intermittently.

Apply the suggested fix to normalize base64url to standard base64:

Base64url normalization
 export const decodeToken = (token: string): JwtPayload | null => {
   try {
     const part = token.split('.')[1]
     if (!part) return null
-    return JSON.parse(atob(part)) as JwtPayload
+    const base64 = part.replace(/-/g, '+').replace(/_/g, '/')
+    const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4)
+    return JSON.parse(atob(padded)) as JwtPayload
   } catch {
     return null
   }
 }
📝 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
export const decodeToken = (token: string): JwtPayload | null => {
try {
const part = token.split('.')[1]
if (!part) return null
return JSON.parse(atob(part)) as JwtPayload
} catch {
export const decodeToken = (token: string): JwtPayload | null => {
try {
const part = token.split('.')[1]
if (!part) return null
const base64 = part.replace(/-/g, '+').replace(/_/g, '/')
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4)
return JSON.parse(atob(padded)) as JwtPayload
} catch {
return null
}
}
🤖 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 `@apps/interview-portal-fe/src/lib/token.ts` around lines 58 - 63, The
decodeToken function uses atob() to decode the JWT payload segment, but JWT
payloads use base64url encoding (RFC 7518) which replaces `+` with `-` and `/`
with `_`, and omits padding. The atob() function expects standard base64, so it
fails on tokens with `-` or `_` characters. Fix this by normalizing the
base64url string before calling atob(): replace all `-` characters with `+`,
replace all `_` characters with `/`, add padding with `=` characters as needed
to make the length a multiple of 4, then pass the normalized string to atob().

Comment on lines +3 to +10
import path from 'path'

export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify module mode and current alias expression
cat apps/interview-portal-fe/package.json | jq -r '.type // "type field not set"'
rg -nP '\b__dirname\b|fileURLToPath|import\.meta\.url' apps/interview-portal-fe/vite.config.ts

Repository: shivams10/nest-assessment

Length of output: 123


Replace __dirname with ESM-compatible import.meta.url pattern in Vite alias config.

Line 9 uses __dirname, which is undefined in ESM contexts. Since the project uses "type": "module", this will throw a ReferenceError at runtime, breaking app startup and all @/... imports.

Suggested fix
-import path from 'path'
+import { fileURLToPath, URL } from 'node:url'
@@
   resolve: {
     alias: {
-      '@': path.resolve(__dirname, './src'),
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
     },
   },
 })
📝 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
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
🤖 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 `@apps/interview-portal-fe/vite.config.ts` around lines 3 - 10, The `__dirname`
variable used in the path.resolve call within the alias configuration is
undefined in ESM contexts, which will cause a ReferenceError at runtime. To fix
this, import `fileURLToPath` from the 'url' module, then create an
ESM-compatible `__dirname` replacement by combining
`fileURLToPath(import.meta.url)` with `path.dirname()`. Finally, replace the
existing `__dirname` reference in the alias configuration with this new
variable.

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.

1 participant