Skip to content

Feature/teacher student loginn - #4

Open
lenhatminh2102 wants to merge 8 commits into
mainfrom
feature/teacher-student-loginn
Open

lenhatminh2102 wants to merge 8 commits into
mainfrom
feature/teacher-student-loginn

Conversation

@lenhatminh2102

@lenhatminh2102 lenhatminh2102 commented Jul 8, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Super Admin now uses live API data to manage admins, sessions, whitelist entries, and lecturer requests (including approve/reject + temporary credentials).
    • Added “Đăng ký Giảng viên” page to submit lecturer requests with validation and feedback.
    • Student and Teacher areas are now API-driven for syllabi search/view and teacher document management; chatbot now supports course-scoped sessions.
  • Bug Fixes
    • Improved session-aware role redirects and more reliable logout across roles.
  • Documentation
    • Added a detailed integration plan for the Super Admin real API transition.
  • Chores
    • Improved backend fallback behavior for AI response streaming during external service failures.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces mock authentication and local data flows with backend sessions and API clients across administration, syllabus, document, and chatbot features. It adds Super Admin management flows, lecturer registration and review, live syllabus rendering, streaming chat, seed data, Supabase factories, and RAG fallback handling.

Changes

Authentication and Administration

Layer / File(s) Summary
Integration plan
plans/.../plan.md
Documents API routes, architecture, phased implementation, smoke tests, risks, and decisions.
Session and API foundations
web/lib/auth-client.ts, web/app/contexts/AuthContext.tsx
Configures Better Auth admin roles, adds typed API errors and lecturer-request contracts, and uses backend session state with role-based portal paths.
Login, route gating, and layouts
web/app/login/page.tsx, web/components/ProtectedRoute.tsx, web/app/*/layout.tsx
Updates login, redirects, loading gates, role names, and asynchronous logout behavior.
Lecturer registration
web/app/register-teacher/page.tsx
Validates and submits lecturer registration requests through the API.
Super Admin management
web/app/superadmin/page.tsx, web/app/superadmin/admins/...
Adds API-backed dashboard statistics, user listing and creation, account actions, impersonation, and session revocation.
Whitelist and lecturer requests
web/app/superadmin/whitelist/page.tsx, web/app/superadmin/lecturer-requests/page.tsx
Adds paginated whitelist CRUD/import/export and lecturer-request approval flows with temporary credentials.

Syllabus, Documents, and Chatbot

Layer / File(s) Summary
Learning API client
web/lib/api.ts
Adds typed syllabus, document, course, chat-session, authentication, upload, deletion, and streaming endpoints.
Student syllabus experience
web/app/student/page.tsx, web/app/student/syllabus/..., web/components/student/*
Loads live subjects and syllabus details, maps API data, and renders card-based search plus tabbed syllabus content.
Teacher management
web/app/teacher/syllabus/page.tsx, web/app/teacher/documents/page.tsx
Replaces mock syllabus and document management with API loading, PDF upload, deletion, filtering, and status handling.
Streaming chatbot
web/components/chatbot/*
Creates scoped chat sessions, streams responses and citations, loads history, and composes reusable chat UI components.

Backend Setup

Layer / File(s) Summary
Supabase and environment configuration
api/src/config/supabase.ts, web/lib/supabase-client.ts, web/next.config.ts
Adds validated Supabase factories and startup environment loading.
Seeded syllabus and test data
api/prisma/seed.ts, credentials.json
Adds test whitelist accounts, PRN232, JSON-backed syllabus records, related documents, and updated seed credentials.
RAG generation fallback
api/src/modules/rag/services/rag.service.ts
Falls back from AnythingLLM to course-scoped Gemini streaming and emits a technical error if both fail.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant LoginPage
  participant AuthContext
  participant Backend
  participant ProtectedRoute
  User->>LoginPage: submit credentials
  LoginPage->>AuthContext: login(email, password)
  AuthContext->>Backend: authenticate and load session
  Backend-->>AuthContext: session and role
  AuthContext-->>LoginPage: authentication result
  LoginPage->>ProtectedRoute: navigate to role portal
  ProtectedRoute->>AuthContext: read loading and role state
  AuthContext-->>ProtectedRoute: session state
  ProtectedRoute-->>User: render portal or redirect
Loading
sequenceDiagram
  participant Admin
  participant LecturerRequestsPage
  participant BackendAPI
  Admin->>LecturerRequestsPage: approve lecturer request
  LecturerRequestsPage->>BackendAPI: POST approval endpoint
  BackendAPI-->>LecturerRequestsPage: temporary credentials
  LecturerRequestsPage-->>Admin: display credentials modal
Loading
sequenceDiagram
  participant Student
  participant ChatbotWidget
  participant ChatAPI
  participant ChatMessageList
  Student->>ChatbotWidget: send question
  ChatbotWidget->>ChatAPI: create scoped session
  ChatbotWidget->>ChatAPI: stream question
  ChatAPI-->>ChatbotWidget: response chunks and citations
  ChatbotWidget->>ChatMessageList: update message
  ChatMessageList-->>Student: render streamed answer
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the auth/login overhaul for teacher and student portals, though it understates the broader superadmin and API integration work.
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.
✨ 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/teacher-student-loginn

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Integrate Better Auth sessions with real admin/whitelist/lecturer-request APIs

✨ Enhancement 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Replace mock/localStorage auth with Better Auth session + role-based redirects
• Add Super Admin real API pages: users list/detail, whitelist CRUD/import, lecturer requests
 approval
• Align roles to backend (ADMIN/LECTURER/STUDENT) across login and route guards
Diagram

graph TD
A["Auth Pages (Login/Register)"] --> B["AuthContext (useSession)"] --> C["Auth Client (Better Auth + admin plugin + apiFetch)"] --> D["Better Auth API (/api/auth/*)"]
E["ProtectedRoute"] --> B
F["Superadmin Pages"] --> C --> G["Whitelist API (/api/whitelist)"]
C --> H["Auth-Admin API (/api/auth-admin/*)"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Move route protection to Next.js middleware / server components
  • ➕ Prevents protected page HTML from ever rendering for unauthorized users
  • ➕ Reduces client-side redirect flashes and improves perceived performance
  • ➕ Centralizes auth checks for all routes
  • ➖ More complex to wire with Better Auth session verification on the edge/server
  • ➖ May require additional backend/session introspection endpoints or helpers
2. Use server-side role filtering for listUsers (avoid client filtering)
  • ➕ Correct pagination totals when filtering by role
  • ➕ Less data transfer vs fetching up to 500 users and filtering client-side
  • ➖ Depends on admin plugin filter support and consistent API behavior
  • ➖ May require backend changes if not supported
3. Centralize role coercion (toUserRole) into a single shared utility
  • ➕ Avoids duplicated role parsing logic across AuthContext/Login
  • ➕ Reduces risk of inconsistent fallback behavior
  • ➖ Minor refactor; limited functional impact compared to the current implementation

Recommendation: The PR’s approach (client-side session-driven AuthContext + Better Auth adminClient + apiFetch for non-auth REST) is pragmatic and consistent with existing UI patterns. Consider follow-up work to move protection into middleware/server for stronger guarantees, and to replace the current client-side role filtering/pagination workaround with true server-side filtering once supported reliably.

Files changed (14) +2791 / -722

Enhancement (9) +2364 / -481
auth-client.tsEnable Better Auth admin plugin and add shared REST apiFetch helper +92/-2

Enable Better Auth admin plugin and add shared REST apiFetch helper

• Adds Better Auth adminClient plugin with role access control mapping (ADMIN/LECTURER/STUDENT). Introduces apiBaseUrl export, ApiError, apiFetch() with credentials included, and typed interfaces for lecturer-request API payloads/responses.

web/lib/auth-client.ts

page.tsxSwitch login to Better Auth and simplify Google sign-in +48/-150

Switch login to Better Auth and simplify Google sign-in

• Replaces demo role selection and mock Google login with Better Auth credential sign-in and social sign-in (Google). After successful auth, fetches session and redirects based on backend role; adds link to lecturer registration.

web/app/login/page.tsx

page.tsxAdd lecturer registration request page (public) +330/-0

Add lecturer registration request page (public)

• New page to submit lecturer-request payload (name/email/reason) to /api/auth-admin/lecturer-request using apiFetch. Adds validation and success/error notifications to support onboarding flow for lecturers.

web/app/register-teacher/page.tsx

layout.tsxLock superadmin area to ADMIN and add lecturer-requests nav item +5/-3

Lock superadmin area to ADMIN and add lecturer-requests nav item

• Updates ProtectedRoute allowedRoles to ADMIN and makes logout async. Extends navigation to include a Lecturer Requests page entry.

web/app/superadmin/layout.tsx

page.tsxFetch real dashboard stats via admin plugin and whitelist API +180/-92

Fetch real dashboard stats via admin plugin and whitelist API

• Replaces hardcoded stats with live counts from Better Auth admin listUsers and /api/whitelist pagination totals. Adds loading state, refresh button, and updates stat cards/shortcuts copy to reflect real data.

web/app/superadmin/page.tsx

page.tsxReplace mock admin list with real user management (list/search/filter/create) +282/-127

Replace mock admin list with real user management (list/search/filter/create)

• Reworks the page to call authClient.admin.listUsers with pagination/search, adds role filter (with client-side fallback filtering), and introduces a Create User modal supporting role selection and optional password. Adds notifications, loading states, and link to per-user detail page.

web/app/superadmin/admins/page.tsx

page.tsxAdd user detail admin actions (role/ban/password/delete/impersonate/sessions) +611/-0

Add user detail admin actions (role/ban/password/delete/impersonate/sessions)

• New detail page that loads user data and sessions via Better Auth admin plugin endpoints. Provides guarded admin actions with confirm modals, disallows self-ban/delete/role-change/impersonation, and supports session revocation and post-impersonation portal redirect.

web/app/superadmin/admins/[id]/page.tsx

page.tsxIntegrate whitelist CRUD/import/export with real REST API and pagination +282/-107

Integrate whitelist CRUD/import/export with real REST API and pagination

• Replaces local state with server-backed pagination and search using /api/whitelist. Adds create/delete/import/export flows via apiFetch, swaps alert/confirm for Mantine notifications and confirm modals, and introduces loading indicators and counters.

web/app/superadmin/whitelist/page.tsx

page.tsxAdd lecturer requests review UI with approve/reject and credentials modal +534/-0

Add lecturer requests review UI with approve/reject and credentials modal

• New page that lists lecturer requests from /api/auth-admin/admin/lecturer-requests, filters by status, and supports approve/reject with confirm modals. On approval, shows one-time credentials (temporary password) with copy-to-clipboard UX and notifications.

web/app/superadmin/lecturer-requests/page.tsx

Bug fix (1) +11 / -13
ProtectedRoute.tsxUpdate ProtectedRoute to use backend role mapping and loading state +11/-13

Update ProtectedRoute to use backend role mapping and loading state

• Adds isLoading handling to avoid redirects during session fetch. Replaces hardcoded role redirects with portalPathForRole() and updates allowedRoles typing to backend roles.

web/components/ProtectedRoute.tsx

Refactor (3) +87 / -228
AuthContext.tsxRefactor AuthContext to derive user from Better Auth session +81/-222

Refactor AuthContext to derive user from Better Auth session

• Removes mock users and localStorage persistence, switching to authClient.useSession() for reactive session state. Adds async login/logout using Better Auth, exposes isLoading and refetchSession, and standardizes roles to ADMIN/LECTURER/STUDENT with portalPathForRole().

web/app/contexts/AuthContext.tsx

layout.tsxAlign teacher portal guard to LECTURER role and async logout +3/-3

Align teacher portal guard to LECTURER role and async logout

• Updates ProtectedRoute allowedRoles from teacher to LECTURER to match backend. Converts logout handler to async to await server sign-out before redirecting.

web/app/teacher/layout.tsx

layout.tsxAlign student portal guard to STUDENT role and async logout +3/-3

Align student portal guard to STUDENT role and async logout

• Updates ProtectedRoute allowedRoles from student to STUDENT to match backend. Converts logout handler to async to await server sign-out before redirecting.

web/app/student/layout.tsx

Documentation (1) +329 / -0
plan.mdAdd implementation plan for Super Admin real API integration +329/-0

Add implementation plan for Super Admin real API integration

• Introduces an approved, phased plan describing migration from mock/localStorage to Better Auth sessions, adminClient plugin usage, REST endpoint mapping, and role alignment. Includes sitemap/endpoint map, risk mitigations, and smoke test checklist.

plans/20260708-superadmin-real-api-integration/plan.md

@qodo-code-review

qodo-code-review Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (2) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 90 rules
✅ Skills: 6 invoked
  email-and-password-best-practices
  better-auth-best-practices
  two-factor-authentication-best-practices
  better-auth-security-best-practices
  organization-best-practices
  vercel-react-best-practices

Grey Divider


Action required

1. Impersonation session race 🐞 Bug ☼ Reliability
Description
Admin user detail navigates to the impersonated portal immediately after calling refetchSession()
without awaiting session refresh, so ProtectedRoute may still see the old ADMIN session and redirect
back. This makes impersonation flaky/non-functional depending on timing.
Code

web/app/superadmin/admins/[id]/page.tsx[R295-300]

+          const res = await authClient.admin.impersonateUser({ userId });
+          if (res.error) throw new Error(res.error.message);
+          refetchSession();
+          const role = (detail?.role as UserRole) ?? "STUDENT";
+          router.push(portalPathForRole(role));
+        } catch (err) {
Evidence
Impersonate flow calls refetchSession() and immediately navigates; since refetchSession does not
return a promise, there is no guarantee the session/role is updated before ProtectedRoute checks it
on the destination portal.

web/app/superadmin/admins/[id]/page.tsx[282-300]
web/app/contexts/AuthContext.tsx[62-94]
web/components/ProtectedRoute.tsx[17-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
After `authClient.admin.impersonateUser`, the code calls `refetchSession()` (fire-and-forget) and immediately `router.push(...)`. Because route guards depend on session role, the new page can load with the stale ADMIN session and bounce back.

## Issue Context
- `refetchSession` is currently typed/implemented as void and cannot be awaited.
- `ProtectedRoute` redirects based on the current `user.role`.

## Fix Focus Areas
- web/app/superadmin/admins/[id]/page.tsx[282-310]
- web/app/contexts/AuthContext.tsx[62-95]
- web/components/ProtectedRoute.tsx[17-35]

### Suggested implementation direction
- Change `refetchSession` to return a `Promise` (`refetchSession: () => Promise<void>`), implemented as `return refetch()`.
- In `handleImpersonate`, `await refetchSession()` and then read the *fresh* session/user role (from `useAuth().user` after refresh, or by calling `authClient.getSession()` and validating the role).
- Redirect after the role is confirmed; consider `window.location.href = portalPathForRole(role)` to force a full reload if client-side navigation still races cookie/session state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Login redirect misroutes users 🐞 Bug ≡ Correctness
Description
LoginPage redirects by calling authClient.getSession() and defaulting a missing/unknown role to
STUDENT, which can route ADMIN/LECTURER to the wrong portal if the session payload lacks role.
Additionally, Google OAuth sets callbackURL to /login but the page has no “already authenticated
→ redirect” logic, so users can land back on the login screen after successful OAuth.
Code

web/app/login/page.tsx[R45-53]

+  const redirectAfterAuth = async () => {
+    const session = await authClient.getSession();
+    const role = toUserRole(session.data?.user?.role);
+    router.push(portalPathForRole(role));
+  };
+
  const handleCredentialsLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!email || !password) {
Evidence
LoginPage derives role from getSession() and falls back to STUDENT, while the API docs show
get-session example response without role, making this fallback reachable and potentially wrong.
Google OAuth callbackURL points back to /login with no subsequent redirect logic in the page.

web/app/login/page.tsx[45-86]
docs/api/00_auth.md[142-160]
docs/api/00_auth.md[92-107]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`redirectAfterAuth()` derives role from `getSession()` and falls back to `STUDENT`, which can misroute users when session responses don’t include `role`. Google OAuth returns to `/login` but the page does not auto-redirect authenticated users, leaving them stuck.

## Issue Context
- API docs show `get-session` example response omits `user.role`, while `sign-in/email` response includes it.
- Current code uses `/login` as OAuth callbackURL.

## Fix Focus Areas
- web/app/login/page.tsx[45-86]
- web/app/contexts/AuthContext.tsx[62-81]
- docs/api/00_auth.md[92-107]
- docs/api/00_auth.md[142-160]

### Suggested implementation direction
1) Credentials login:
- Change `AuthContext.login()` to return the logged-in user (or at least `role`) from `authClient.signIn.email` response.
- In `LoginPage`, redirect using that returned role (no extra `getSession()` round-trip).

2) Google OAuth:
- Keep callbackURL as `/login` **or** set it to a dedicated callback route, but add a `useEffect` in `LoginPage` that checks session/auth state (e.g., `useAuth()`/`authClient.useSession()`) and immediately redirects to `portalPathForRole(user.role)` when authenticated.
- Avoid defaulting missing role to STUDENT; if role is missing, show an error and require re-auth or fetch a role-bearing endpoint.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Admin stats capped 500 🐞 Bug ≡ Correctness
Description
Dashboard computes totalAdmins by fetching only the first 500 users and counting role === ADMIN,
so it undercounts admins when there are more than 500 total users. This makes dashboard statistics
incorrect at scale.
Code

web/app/superadmin/page.tsx[R83-96]

+      const [usersPageRes, whitelistRes, usersListRes] = await Promise.all([
+        authClient.admin.listUsers({ query: { limit: "1", offset: "0" } }),
+        apiFetch<WhitelistListResponse>("/api/whitelist?page=1&limit=1"),
+        authClient.admin.listUsers({ query: { limit: "500", offset: "0" } }),
+      ]);
+
+      if (usersPageRes.error) {
+        throw new Error(usersPageRes.error.message ?? "Không tải được danh sách người dùng");
+      }
+
+      const totalUsers = usersPageRes.data?.total ?? 0;
+      const users = usersListRes.data?.users ?? [];
+      const totalAdmins = users.filter((u) => u.role === "ADMIN").length;
+      const totalWhitelist = whitelistRes.pagination.total;
Evidence
The dashboard hardcodes limit: "500" for the users list used to compute admin count, while
list-users is paginated and can exceed that number.

web/app/superadmin/page.tsx[80-97]
docs/api/00_auth.md[376-399]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Dashboard derives `totalAdmins` from a `listUsers(limit=500)` call, so the admin count is truncated when user count exceeds 500.

## Issue Context
`list-users` is paginated; there is no documented role filter to request `ADMIN` users directly.

## Fix Focus Areas
- web/app/superadmin/page.tsx[80-99]
- docs/api/00_auth.md[376-399]

### Suggested implementation direction
- Use `usersPageRes.data.total` to know total user count.
- Iterate `listUsers` in pages (e.g., limit=100/200) until `offset >= total`, accumulating `ADMIN` count.
- Alternatively, add a backend endpoint for counts by role (best long-term), then call that from the dashboard instead of scanning users client-side.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. apiFetch merge overwritten 🐞 Bug ☼ Reliability
Description
In web/lib/auth-client.ts, apiFetch merges headers but then spreads ...init afterwards, allowing
caller-provided init (especially headers/credentials) to overwrite the merged defaults and break
requests unexpectedly. This makes the helper unreliable as soon as any call passes custom
headers/options.
Code

web/lib/auth-client.ts[R39-46]

+    res = await fetch(`${apiBaseUrl}${path}`, {
+      credentials: "include",
+      headers: {
+        "Content-Type": "application/json",
+        ...init?.headers,
+      },
+      ...init,
+    });
Evidence
The helper merges init?.headers into a header object but then spreads ...init after, which
overwrites the already-merged headers field (and any other defaults).

web/lib/auth-client.ts[31-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`apiFetch` constructs a request object with default `credentials` and merged `headers`, but then spreads `...init` *after* those fields. In JS object spread, later properties win, so `init.headers` can overwrite the merged header object and `init.credentials` can overwrite `"include"`.

## Issue Context
This helper is intended to standardize REST calls to non–Better Auth endpoints (whitelist, lecturer-requests). It should be safe even when callers pass `headers`.

## Fix Focus Areas
- web/lib/auth-client.ts[32-62]

### Suggested implementation direction
- Build `const headers = new Headers(init?.headers)`.
- Only set `Content-Type: application/json` when sending a JSON body (or when caller didn’t provide it).
- Call `fetch(url, { ...init, credentials: "include", headers })` (note `...init` comes *before* the enforced defaults, or explicitly override after it).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Role filter capped 500 🐞 Bug ≡ Correctness
Description
Superadmin user list implements role filtering by fetching only the first 500 users and filtering
client-side, so results and total counts become wrong once the system has more than 500 users.
Pagination under a role filter is therefore incorrect for larger datasets.
Code

web/app/superadmin/admins/page.tsx[R94-117]

+      const searchField = searchTerm.includes("@") ? "email" : "name";
+      const baseQuery: Record<string, string> = {
+        limit: roleFilter === "ALL" ? String(PAGE_SIZE) : "500",
+        offset: roleFilter === "ALL" ? String((page - 1) * PAGE_SIZE) : "0",
+      };
+      if (searchTerm) {
+        baseQuery.searchValue = searchTerm;
+        baseQuery.searchField = searchField;
+      }
+
+      const res = await authClient.admin.listUsers({ query: baseQuery });
+      if (res.error) {
+        throw new Error(res.error.message ?? "Không tải được danh sách người dùng");
+      }
+
+      let list = (res.data?.users ?? []) as AdminListUser[];
+      let count = res.data?.total ?? list.length;
+
+      if (roleFilter !== "ALL") {
+        list = list.filter((u) => u.role === roleFilter);
+        count = list.length;
+        const start = (page - 1) * PAGE_SIZE;
+        list = list.slice(start, start + PAGE_SIZE);
+      }
Evidence
The code explicitly uses a 500-item cap for role filtering, while the API contract describes
list-users as paginated and therefore unbounded beyond any single page size.

web/app/superadmin/admins/page.tsx[91-117]
docs/api/00_auth.md[376-399]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `roleFilter !== "ALL"`, the page requests `limit: "500"` and then filters/slices locally. This silently truncates datasets >500 users and yields incorrect `total`/pagination.

## Issue Context
`list-users` is a paginated endpoint (`limit`/`offset`) and does not document a role filter parameter, so the UI must either:
- iterate pages until it has accurate counts, or
- remove/disable role filtering until backend supports role filtering.

## Fix Focus Areas
- web/app/superadmin/admins/page.tsx[91-129]
- docs/api/00_auth.md[376-399]

### Suggested implementation direction
Option A (accurate, potentially more calls):
- When roleFilter is set, loop over `listUsers` pages (using `limit/offset`) accumulating matches and counting total matches; stop early once you collected enough matches to render the requested page plus know total count.

Option B (simpler UX, avoids wrong data):
- Disable the role filter UI (or show a warning) until backend adds a server-side role filter endpoint/query param.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
6. baseQuery uses Record 📘 Rule violation ⚙ Maintainability
Description
The listUsers query parameters are assembled into a generic Record<string, string> instead of a
named query-parameter type, weakening compile-time guarantees for API parameters. This violates the
requirement to define and use TypeScript types for API parameters in changed code.
Code

web/app/superadmin/admins/page.tsx[R94-102]

+      const searchField = searchTerm.includes("@") ? "email" : "name";
+      const baseQuery: Record<string, string> = {
+        limit: roleFilter === "ALL" ? String(PAGE_SIZE) : "500",
+        offset: roleFilter === "ALL" ? String((page - 1) * PAGE_SIZE) : "0",
+      };
+      if (searchTerm) {
+        baseQuery.searchValue = searchTerm;
+        baseQuery.searchField = searchField;
+      }
Evidence
PR Compliance ID 1287031 requires API parameters to be represented via named TypeScript types. The
added code uses Record<string, string> for the query object and mutates it with additional keys,
rather than defining a dedicated query parameter interface/type.

Rule 1287031: Define TypeScript types for API payloads, parameters, and RAG search results
web/app/superadmin/admins/page.tsx[94-104]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The admin users listing constructs API query parameters as `Record<string, string>`, which is not a named type describing the expected parameter shape.

## Issue Context
This PR added `authClient.admin.listUsers({ query: baseQuery })` usage with `baseQuery` being dynamically extended (`searchValue`, `searchField`). The compliance rule requires named TypeScript types for API parameters.

## Fix Focus Areas
- web/app/superadmin/admins/page.tsx[94-104]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Whitelist payload types missing 📘 Rule violation ⚙ Maintainability
Description
Whitelist REST requests are constructed with inline object literals instead of a named request
payload type, reducing type safety and consistency across API contracts. This violates the
requirement to define and use TypeScript types for API payloads/parameters/results in changed code.
Code

web/app/superadmin/whitelist/page.tsx[R133-136]

+      await apiFetch("/api/whitelist", {
+        method: "POST",
+        body: JSON.stringify({ email }),
+      });
Evidence
PR Compliance ID 1287031 requires API payloads/parameters/results to use named TypeScript types. The
whitelist mutation requests serialize inline payloads ({ email }, { emails: emailList }) rather
than using named payload types.

Rule 1287031: Define TypeScript types for API payloads, parameters, and RAG search results
web/app/superadmin/whitelist/page.tsx[133-136]
web/app/superadmin/whitelist/page.tsx[200-203]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`apiFetch` calls for whitelist mutations build request bodies with inline object literals (e.g., `JSON.stringify({ email })`). The compliance rule requires using named TypeScript `type`/`interface` definitions for API payloads/parameters/results.

## Issue Context
This PR introduced new REST calls to `/api/whitelist` and `/api/whitelist/import`. Response types are defined, but request payloads are not.

## Fix Focus Areas
- web/app/superadmin/whitelist/page.tsx[133-136]
- web/app/superadmin/whitelist/page.tsx[200-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +133 to +136
await apiFetch("/api/whitelist", {
method: "POST",
body: JSON.stringify({ email }),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Whitelist payload types missing 📘 Rule violation ⚙ Maintainability

Whitelist REST requests are constructed with inline object literals instead of a named request
payload type, reducing type safety and consistency across API contracts. This violates the
requirement to define and use TypeScript types for API payloads/parameters/results in changed code.
Agent Prompt
## Issue description
`apiFetch` calls for whitelist mutations build request bodies with inline object literals (e.g., `JSON.stringify({ email })`). The compliance rule requires using named TypeScript `type`/`interface` definitions for API payloads/parameters/results.

## Issue Context
This PR introduced new REST calls to `/api/whitelist` and `/api/whitelist/import`. Response types are defined, but request payloads are not.

## Fix Focus Areas
- web/app/superadmin/whitelist/page.tsx[133-136]
- web/app/superadmin/whitelist/page.tsx[200-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +94 to +102
const searchField = searchTerm.includes("@") ? "email" : "name";
const baseQuery: Record<string, string> = {
limit: roleFilter === "ALL" ? String(PAGE_SIZE) : "500",
offset: roleFilter === "ALL" ? String((page - 1) * PAGE_SIZE) : "0",
};
if (searchTerm) {
baseQuery.searchValue = searchTerm;
baseQuery.searchField = searchField;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. basequery uses record 📘 Rule violation ⚙ Maintainability

The listUsers query parameters are assembled into a generic Record<string, string> instead of a
named query-parameter type, weakening compile-time guarantees for API parameters. This violates the
requirement to define and use TypeScript types for API parameters in changed code.
Agent Prompt
## Issue description
The admin users listing constructs API query parameters as `Record<string, string>`, which is not a named type describing the expected parameter shape.

## Issue Context
This PR added `authClient.admin.listUsers({ query: baseQuery })` usage with `baseQuery` being dynamically extended (`searchValue`, `searchField`). The compliance rule requires named TypeScript types for API parameters.

## Fix Focus Areas
- web/app/superadmin/admins/page.tsx[94-104]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread web/lib/auth-client.ts
Comment on lines +39 to +46
res = await fetch(`${apiBaseUrl}${path}`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
...init?.headers,
},
...init,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Apifetch merge overwritten 🐞 Bug ☼ Reliability

In web/lib/auth-client.ts, apiFetch merges headers but then spreads ...init afterwards, allowing
caller-provided init (especially headers/credentials) to overwrite the merged defaults and break
requests unexpectedly. This makes the helper unreliable as soon as any call passes custom
headers/options.
Agent Prompt
## Issue description
`apiFetch` constructs a request object with default `credentials` and merged `headers`, but then spreads `...init` *after* those fields. In JS object spread, later properties win, so `init.headers` can overwrite the merged header object and `init.credentials` can overwrite `"include"`.

## Issue Context
This helper is intended to standardize REST calls to non–Better Auth endpoints (whitelist, lecturer-requests). It should be safe even when callers pass `headers`.

## Fix Focus Areas
- web/lib/auth-client.ts[32-62]

### Suggested implementation direction
- Build `const headers = new Headers(init?.headers)`.
- Only set `Content-Type: application/json` when sending a JSON body (or when caller didn’t provide it).
- Call `fetch(url, { ...init, credentials: "include", headers })` (note `...init` comes *before* the enforced defaults, or explicitly override after it).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread web/app/login/page.tsx
Comment on lines +45 to 53
const redirectAfterAuth = async () => {
const session = await authClient.getSession();
const role = toUserRole(session.data?.user?.role);
router.push(portalPathForRole(role));
};

const handleCredentialsLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!email || !password) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Login redirect misroutes users 🐞 Bug ≡ Correctness

LoginPage redirects by calling authClient.getSession() and defaulting a missing/unknown role to
STUDENT, which can route ADMIN/LECTURER to the wrong portal if the session payload lacks role.
Additionally, Google OAuth sets callbackURL to /login but the page has no “already authenticated
→ redirect” logic, so users can land back on the login screen after successful OAuth.
Agent Prompt
## Issue description
`redirectAfterAuth()` derives role from `getSession()` and falls back to `STUDENT`, which can misroute users when session responses don’t include `role`. Google OAuth returns to `/login` but the page does not auto-redirect authenticated users, leaving them stuck.

## Issue Context
- API docs show `get-session` example response omits `user.role`, while `sign-in/email` response includes it.
- Current code uses `/login` as OAuth callbackURL.

## Fix Focus Areas
- web/app/login/page.tsx[45-86]
- web/app/contexts/AuthContext.tsx[62-81]
- docs/api/00_auth.md[92-107]
- docs/api/00_auth.md[142-160]

### Suggested implementation direction
1) Credentials login:
- Change `AuthContext.login()` to return the logged-in user (or at least `role`) from `authClient.signIn.email` response.
- In `LoginPage`, redirect using that returned role (no extra `getSession()` round-trip).

2) Google OAuth:
- Keep callbackURL as `/login` **or** set it to a dedicated callback route, but add a `useEffect` in `LoginPage` that checks session/auth state (e.g., `useAuth()`/`authClient.useSession()`) and immediately redirects to `portalPathForRole(user.role)` when authenticated.
- Avoid defaulting missing role to STUDENT; if role is missing, show an error and require re-auth or fetch a role-bearing endpoint.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +295 to +300
const res = await authClient.admin.impersonateUser({ userId });
if (res.error) throw new Error(res.error.message);
refetchSession();
const role = (detail?.role as UserRole) ?? "STUDENT";
router.push(portalPathForRole(role));
} catch (err) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Impersonation session race 🐞 Bug ☼ Reliability

Admin user detail navigates to the impersonated portal immediately after calling refetchSession()
without awaiting session refresh, so ProtectedRoute may still see the old ADMIN session and redirect
back. This makes impersonation flaky/non-functional depending on timing.
Agent Prompt
## Issue description
After `authClient.admin.impersonateUser`, the code calls `refetchSession()` (fire-and-forget) and immediately `router.push(...)`. Because route guards depend on session role, the new page can load with the stale ADMIN session and bounce back.

## Issue Context
- `refetchSession` is currently typed/implemented as void and cannot be awaited.
- `ProtectedRoute` redirects based on the current `user.role`.

## Fix Focus Areas
- web/app/superadmin/admins/[id]/page.tsx[282-310]
- web/app/contexts/AuthContext.tsx[62-95]
- web/components/ProtectedRoute.tsx[17-35]

### Suggested implementation direction
- Change `refetchSession` to return a `Promise` (`refetchSession: () => Promise<void>`), implemented as `return refetch()`.
- In `handleImpersonate`, `await refetchSession()` and then read the *fresh* session/user role (from `useAuth().user` after refresh, or by calling `authClient.getSession()` and validating the role).
- Redirect after the role is confirmed; consider `window.location.href = portalPathForRole(role)` to force a full reload if client-side navigation still races cookie/session state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +94 to +117
const searchField = searchTerm.includes("@") ? "email" : "name";
const baseQuery: Record<string, string> = {
limit: roleFilter === "ALL" ? String(PAGE_SIZE) : "500",
offset: roleFilter === "ALL" ? String((page - 1) * PAGE_SIZE) : "0",
};
if (searchTerm) {
baseQuery.searchValue = searchTerm;
baseQuery.searchField = searchField;
}

const res = await authClient.admin.listUsers({ query: baseQuery });
if (res.error) {
throw new Error(res.error.message ?? "Không tải được danh sách người dùng");
}

let list = (res.data?.users ?? []) as AdminListUser[];
let count = res.data?.total ?? list.length;

if (roleFilter !== "ALL") {
list = list.filter((u) => u.role === roleFilter);
count = list.length;
const start = (page - 1) * PAGE_SIZE;
list = list.slice(start, start + PAGE_SIZE);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Role filter capped 500 🐞 Bug ≡ Correctness

Superadmin user list implements role filtering by fetching only the first 500 users and filtering
client-side, so results and total counts become wrong once the system has more than 500 users.
Pagination under a role filter is therefore incorrect for larger datasets.
Agent Prompt
## Issue description
When `roleFilter !== "ALL"`, the page requests `limit: "500"` and then filters/slices locally. This silently truncates datasets >500 users and yields incorrect `total`/pagination.

## Issue Context
`list-users` is a paginated endpoint (`limit`/`offset`) and does not document a role filter parameter, so the UI must either:
- iterate pages until it has accurate counts, or
- remove/disable role filtering until backend supports role filtering.

## Fix Focus Areas
- web/app/superadmin/admins/page.tsx[91-129]
- docs/api/00_auth.md[376-399]

### Suggested implementation direction
Option A (accurate, potentially more calls):
- When roleFilter is set, loop over `listUsers` pages (using `limit/offset`) accumulating matches and counting total matches; stop early once you collected enough matches to render the requested page plus know total count.

Option B (simpler UX, avoids wrong data):
- Disable the role filter UI (or show a warning) until backend adds a server-side role filter endpoint/query param.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +83 to +96
const [usersPageRes, whitelistRes, usersListRes] = await Promise.all([
authClient.admin.listUsers({ query: { limit: "1", offset: "0" } }),
apiFetch<WhitelistListResponse>("/api/whitelist?page=1&limit=1"),
authClient.admin.listUsers({ query: { limit: "500", offset: "0" } }),
]);

if (usersPageRes.error) {
throw new Error(usersPageRes.error.message ?? "Không tải được danh sách người dùng");
}

const totalUsers = usersPageRes.data?.total ?? 0;
const users = usersListRes.data?.users ?? [];
const totalAdmins = users.filter((u) => u.role === "ADMIN").length;
const totalWhitelist = whitelistRes.pagination.total;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

7. Admin stats capped 500 🐞 Bug ≡ Correctness

Dashboard computes totalAdmins by fetching only the first 500 users and counting role === ADMIN,
so it undercounts admins when there are more than 500 total users. This makes dashboard statistics
incorrect at scale.
Agent Prompt
## Issue description
Dashboard derives `totalAdmins` from a `listUsers(limit=500)` call, so the admin count is truncated when user count exceeds 500.

## Issue Context
`list-users` is paginated; there is no documented role filter to request `ADMIN` users directly.

## Fix Focus Areas
- web/app/superadmin/page.tsx[80-99]
- docs/api/00_auth.md[376-399]

### Suggested implementation direction
- Use `usersPageRes.data.total` to know total user count.
- Iterate `listUsers` in pages (e.g., limit=100/200) until `offset >= total`, accumulating `ADMIN` count.
- Alternatively, add a backend endpoint for counts by role (best long-term), then call that from the dashboard instead of scanning users client-side.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@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: 9

🧹 Nitpick comments (6)
web/app/superadmin/lecturer-requests/page.tsx (2)

84-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a subtler refresh state after approve/reject actions.

loadRequests unconditionally sets loading=true, which replaces the entire table (including the Tabs and filter counts) with a full-page <Loader> every time the list is refreshed after an approve or reject action. This causes a visually jarring full-content swap for what is essentially a background refresh.

Consider adding a separate refreshing state that shows a subtle indicator (e.g., a dimmed overlay or a small spinner in the toolbar) while keeping the table visible:

♻️ Suggested refactor
 export default function LecturerRequestsPage() {
   const [allRequests, setAllRequests] = useState<LecturerRequest[]>([]);
   const [loading, setLoading] = useState(true);
+  const [refreshing, setRefreshing] = useState(false);
   // ...

   const loadRequests = useCallback(async () => {
-    setLoading(true);
+    if (allRequests.length > 0) {
+      setRefreshing(true);
+    } else {
+      setLoading(true);
+    }
     try {
       const data = await apiFetch<LecturerRequestListResponse>(
         "/api/auth-admin/admin/lecturer-requests",
       );
       setAllRequests(data.requests);
     } catch (err) {
       // ...
     } finally {
-      setLoading(false);
+      setLoading(false);
+      setRefreshing(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 `@web/app/superadmin/lecturer-requests/page.tsx` around lines 84 - 105, The
current loadRequests flow in LecturerRequestsPage uses the same loading state
for both initial page load and post-approve/reject refreshes, which forces the
whole table view to disappear. Split this into a dedicated refreshing state in
the LecturerRequestsPage component and keep loading for the first fetch only,
then have loadRequests set the subtle refresh state when reloading after actions
so the Tabs, counts, and table remain visible with a lighter indicator instead
of the full-page Loader.

303-375: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Empty table state is rendered outside the <Table.Tbody>.

When filteredRequests is empty, the <Table.Tbody> renders zero rows (only headers visible), and the "Không có yêu cầu nào phù hợp" message at lines 379-385 appears below the table inside the Card. This is functional but produces an empty table body that some screen readers may announce as an empty table. Consider conditionally rendering the empty state instead of the table when filteredRequests.length === 0:

♻️ Suggested approach
           <Card p={0} radius={0} style={{ border: "1px solid `#E2E8F0`", backgroundColor: "white" }}>
-            <Table layout="fixed" highlightOnHover striped>
-              {/* ... thead ... */}
-              <Table.Tbody>
-                {filteredRequests.map((request) => (
-                  /* ... rows ... */
-                ))}
-              </Table.Tbody>
-            </Table>
-
-            {filteredRequests.length === 0 && (
+            {filteredRequests.length === 0 ? (
               <Box p="xl" style={{ textAlign: "center", color: "`#9CA3AF`" }}>
                 <Text size="sm" fw={700}>
                   Không có yêu cầu nào phù hợp
                 </Text>
               </Box>
+            ) : (
+              <Table layout="fixed" highlightOnHover striped>
+                {/* ... existing table content ... */}
+              </Table>
             )}
           </Card>
🤖 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 `@web/app/superadmin/lecturer-requests/page.tsx` around lines 303 - 375, The
empty state for filteredRequests is currently shown outside Table.Tbody, leaving
an empty table body when there are no results. Update the lecturer requests
table in page.tsx so the Table, Table.Tbody, and the “Không có yêu cầu nào phù
hợp” empty message are conditionally rendered based on filteredRequests.length,
using the existing filteredRequests map and the surrounding Table/Card
structure. Keep the normal rows rendering in Table.Tbody when data exists, and
render the empty state instead of the table when there are no matching requests.
web/app/superadmin/admins/[id]/page.tsx (1)

82-82: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Single actionLoading flag makes every action button spin at once.

All admin action buttons (ban/unban, impersonate, remove, save role) bind loading={actionLoading}, so triggering any one action shows a loading spinner on all of them simultaneously. Consider a per-action key (e.g. actionLoading: string | null) if the simultaneous spinners are undesirable.

Also applies to: 491-536

🤖 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 `@web/app/superadmin/admins/`[id]/page.tsx at line 82, The single actionLoading
boolean is shared by all admin actions in the Admin page, so every button
reflects loading at once. Update the state in the page component that renders
the ban/unban, impersonate, remove, and save role actions to track the active
action by key (for example, a string or null) instead of one boolean, and wire
each button’s loading prop to compare against its own action name. Keep the
state transitions localized around the relevant handlers so only the triggered
action shows a spinner.
web/app/superadmin/whitelist/page.tsx (2)

95-117: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Guard against out-of-order responses in loadWhitelist.

There's no cancellation or request-sequencing guard. Rapid page/searchTerm changes (e.g., quick pagination clicks) can resolve fetches out of order and render stale results. The 350 ms debounce only mitigates typing, not pagination. Consider aborting the prior request or ignoring stale responses.

♻️ Ignore stale responses via a request-id guard
+  const requestIdRef = useRef(0);
+
   const loadWhitelist = useCallback(async () => {
+    const requestId = ++requestIdRef.current;
     setLoading(true);
     try {
       const params = new URLSearchParams({
         page: String(page),
         limit: String(PAGE_SIZE),
       });
       if (searchTerm) params.set("q", searchTerm);

       const data = await apiFetch<WhitelistListResponse>(`/api/whitelist?${params.toString()}`);
+      if (requestId !== requestIdRef.current) return;
       setEmails(data.emails);
       setTotal(data.pagination.total);
       setTotalPages(Math.max(1, data.pagination.totalPages));
     } catch (err) {
+      if (requestId !== requestIdRef.current) return;
       const message = err instanceof ApiError ? err.message : "Không tải được whitelist";
       notifications.show({ title: "Lỗi", message, color: "red" });
       setEmails([]);
       setTotal(0);
       setTotalPages(1);
     } finally {
+      if (requestId === requestIdRef.current) setLoading(false);
-      setLoading(false);
     }
   }, [page, searchTerm]);

Add useRef to the React import.

🤖 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 `@web/app/superadmin/whitelist/page.tsx` around lines 95 - 117, Guard
`loadWhitelist` against stale out-of-order responses by adding a
request-sequencing check in `page.tsx`. Introduce a `useRef`-based request id
(or abort controller) near `loadWhitelist`, increment it each time the callback
runs, and only call `setEmails`, `setTotal`, `setTotalPages`, and
`setLoading(false)` for the latest request. Update the React import to include
`useRef`, and keep the existing `useCallback` dependencies aligned with the new
guard.

152-180: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Deleting the last row on the last page leaves an empty page.

onConfirm reloads with the current page. If you delete the only remaining item on the last page, page now exceeds totalPages and the table shows the empty state until the user manually navigates back. Consider stepping page back when the current page becomes empty after deletion.

🤖 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 `@web/app/superadmin/whitelist/page.tsx` around lines 152 - 180, Deleting the
last item on the current last page can leave `page` pointing past the new
`totalPages`, so the whitelist table stays empty after `handleDeleteEmail`
completes. Update the delete flow in `handleDeleteEmail`’s `onConfirm` to reload
data and, if the current page becomes empty and is greater than 1, step the page
back before calling `loadWhitelist()`. Use the existing `page`, `setPage`, and
`loadWhitelist` state/helpers in `web/app/superadmin/whitelist/page.tsx` so the
table always lands on a valid page after deletion.
web/app/register-teacher/page.tsx (1)

80-87: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Public unauthenticated endpoint — confirm backend abuse protection.

The TODO correctly notes this /api/auth-admin/lecturer-request POST is publicly reachable with no auth. Ensure rate-limiting/CAPTCHA is enforced server-side before release to prevent spam/enumeration; client-side validation alone is insufficient.

Want me to open an issue to track adding rate-limiting/CAPTCHA on this endpoint?

🤖 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 `@web/app/register-teacher/page.tsx` around lines 80 - 87, The public POST flow
in register-teacher page uses apiFetch to call /api/auth-admin/lecturer-request
with no auth, so the backend endpoint must be hardened. Add server-side abuse
protection in the lecturer-request handler by enforcing rate limiting and
CAPTCHA verification (and any related anti-spam checks) before accepting
SubmitLecturerRequestResponse submissions; do not rely on client-side validation
alone. Use the /api/auth-admin/lecturer-request route and its backend request
handler as the place to implement and validate this protection.
🤖 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 `@plans/20260708-superadmin-real-api-integration/plan.md`:
- Around line 83-86: The dashboard metrics list currently duplicates GET
/api/whitelist?limit=1, leaving only two distinct data sources. Update the
metric mapping in the plan so the repeated whitelist entry is replaced with the
intended third stat source, keeping authClient.admin.listUsers({
filterField:"role", filterValue:"ADMIN" }) and pagination.total entries unique
and adding the missing dashboard metric alongside them.
- Line 80: The fenced blocks in the plan document are unlabeled, leaving the
markdown lint rule open. Update the two fenced sections in the planning content
to use explicit language tags: label the diagram fence in the diagram section
with mermaid, and label the phase-list fence with text (or remove that second
fence entirely). Use the existing fenced-block content in the plan to locate and
adjust both blocks so all fenced code sections are properly annotated.
- Around line 109-110: The Phase 5 lecturer-requests decision is duplicated and
contradictory across the plan, so consolidate it into one clear source of truth.
Update the Phase 5 section and the checklist in the plan so the decision about
whether to add the lecturer-requests nav/page is stated only once, and make sure
the references around the Phase 5 heading and the lecturer-requests bullet all
match that single decision.

In `@web/app/login/page.tsx`:
- Around line 74-86: The Google sign-in flow in handleGoogleLogin currently
sends users back to the login page via callbackURL, but LoginPage never
redirects authenticated users onward. Update the login page component to detect
an existing session on mount and route users to the correct portal, or change
authClient.signIn.social’s callbackURL to a dedicated post-auth route that
performs the role-based redirect. Use the LoginPage component and
handleGoogleLogin as the main points to fix.

In `@web/app/superadmin/admins/`[id]/page.tsx:
- Around line 295-299: The impersonation flow in the admin page navigates before
the auth session refresh completes because refetchSession is fire-and-forget via
AuthContext. Update the impersonation handler in the page component to await the
session refresh promise before calling router.push, either by returning the
underlying refetch() from refetchSession or invoking refetch() directly here, so
ProtectedRoute sees the updated session before redirecting.

In `@web/app/superadmin/admins/page.tsx`:
- Around line 91-129: The loadUsers callback in admins/page.tsx is still doing
client-side role filtering after fetching a fixed 500-user batch, so pages
beyond that window never surface all matching admins. Update the
authClient.admin.listUsers query path to apply roleFilter server-side (or via an
equivalent API-supported filter) and keep pagination consistent with
page/PAGE_SIZE instead of slicing the in-memory list for non-ALL roles. Make
sure the searchTerm flow and the existing loadUsers callback continue to use the
filtered query while preserving the current error handling and setUsers/setTotal
updates.

In `@web/app/superadmin/page.tsx`:
- Around line 83-96: The dashboard admin count is being computed from a capped
`authClient.admin.listUsers({ limit: "500" })` result, so `totalAdmins` can
undercount as the user base grows. Update the `Promise.all` flow in
`SuperadminPage` to either fetch all users or use a dedicated count-based
approach for admins, and do not rely on the 500-item page for `usersListRes`.
Also handle `usersListRes.error` explicitly, similar to `usersPageRes.error`,
instead of silently defaulting `users` to an empty array.

In `@web/app/superadmin/whitelist/page.tsx`:
- Around line 311-318: The search field in the superadmin whitelist page lacks
an accessible name because the TextInput only uses placeholder text. Update the
TextInput used in the whitelist search UI to include an explicit accessible
name, either by adding an aria-label or by providing a visually hidden label, so
screen readers can identify it reliably.

In `@web/lib/auth-client.ts`:
- Around line 39-46: The fetch options in auth-client’s request helper are
spreading init after the default settings, which allows caller-provided
credentials and headers to overwrite the helper’s defaults. Update the request
construction in the fetch wrapper so the init spread happens before the default
credentials and merged headers, or assign the defaults after init, ensuring
credentials: include and the JSON Content-Type/header merge always remain in
effect.

---

Nitpick comments:
In `@web/app/register-teacher/page.tsx`:
- Around line 80-87: The public POST flow in register-teacher page uses apiFetch
to call /api/auth-admin/lecturer-request with no auth, so the backend endpoint
must be hardened. Add server-side abuse protection in the lecturer-request
handler by enforcing rate limiting and CAPTCHA verification (and any related
anti-spam checks) before accepting SubmitLecturerRequestResponse submissions; do
not rely on client-side validation alone. Use the
/api/auth-admin/lecturer-request route and its backend request handler as the
place to implement and validate this protection.

In `@web/app/superadmin/admins/`[id]/page.tsx:
- Line 82: The single actionLoading boolean is shared by all admin actions in
the Admin page, so every button reflects loading at once. Update the state in
the page component that renders the ban/unban, impersonate, remove, and save
role actions to track the active action by key (for example, a string or null)
instead of one boolean, and wire each button’s loading prop to compare against
its own action name. Keep the state transitions localized around the relevant
handlers so only the triggered action shows a spinner.

In `@web/app/superadmin/lecturer-requests/page.tsx`:
- Around line 84-105: The current loadRequests flow in LecturerRequestsPage uses
the same loading state for both initial page load and post-approve/reject
refreshes, which forces the whole table view to disappear. Split this into a
dedicated refreshing state in the LecturerRequestsPage component and keep
loading for the first fetch only, then have loadRequests set the subtle refresh
state when reloading after actions so the Tabs, counts, and table remain visible
with a lighter indicator instead of the full-page Loader.
- Around line 303-375: The empty state for filteredRequests is currently shown
outside Table.Tbody, leaving an empty table body when there are no results.
Update the lecturer requests table in page.tsx so the Table, Table.Tbody, and
the “Không có yêu cầu nào phù hợp” empty message are conditionally rendered
based on filteredRequests.length, using the existing filteredRequests map and
the surrounding Table/Card structure. Keep the normal rows rendering in
Table.Tbody when data exists, and render the empty state instead of the table
when there are no matching requests.

In `@web/app/superadmin/whitelist/page.tsx`:
- Around line 95-117: Guard `loadWhitelist` against stale out-of-order responses
by adding a request-sequencing check in `page.tsx`. Introduce a `useRef`-based
request id (or abort controller) near `loadWhitelist`, increment it each time
the callback runs, and only call `setEmails`, `setTotal`, `setTotalPages`, and
`setLoading(false)` for the latest request. Update the React import to include
`useRef`, and keep the existing `useCallback` dependencies aligned with the new
guard.
- Around line 152-180: Deleting the last item on the current last page can leave
`page` pointing past the new `totalPages`, so the whitelist table stays empty
after `handleDeleteEmail` completes. Update the delete flow in
`handleDeleteEmail`’s `onConfirm` to reload data and, if the current page
becomes empty and is greater than 1, step the page back before calling
`loadWhitelist()`. Use the existing `page`, `setPage`, and `loadWhitelist`
state/helpers in `web/app/superadmin/whitelist/page.tsx` so the table always
lands on a valid page after deletion.
🪄 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: c1488593-32d9-41e7-b61f-0229e7e400a7

📥 Commits

Reviewing files that changed from the base of the PR and between 3ca4ef0 and 6331c9e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • plans/20260708-superadmin-real-api-integration/plan.md
  • web/app/contexts/AuthContext.tsx
  • web/app/login/page.tsx
  • web/app/register-teacher/page.tsx
  • web/app/student/layout.tsx
  • web/app/superadmin/admins/[id]/page.tsx
  • web/app/superadmin/admins/page.tsx
  • web/app/superadmin/layout.tsx
  • web/app/superadmin/lecturer-requests/page.tsx
  • web/app/superadmin/page.tsx
  • web/app/superadmin/whitelist/page.tsx
  • web/app/teacher/layout.tsx
  • web/components/ProtectedRoute.tsx
  • web/lib/auth-client.ts


## Sitemap & Endpoint Map

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Label the fenced blocks.

Both fenced blocks are unlabeled, which keeps MD040 open. Use mermaid for the diagram and text (or remove the second fence) for the phase list.

♻️ Suggested fix
-```
+```mermaid
...
-```
+```text

Also applies to: 312-314

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 80-80: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@plans/20260708-superadmin-real-api-integration/plan.md` at line 80, The
fenced blocks in the plan document are unlabeled, leaving the markdown lint rule
open. Update the two fenced sections in the planning content to use explicit
language tags: label the diagram fence in the diagram section with mermaid, and
label the phase-list fence with text (or remove that second fence entirely). Use
the existing fenced-block content in the plan to locate and adjust both blocks
so all fenced code sections are properly annotated.

Source: Linters/SAST tools

Comment on lines +83 to +86
├─ authClient.admin.listUsers({ filterField:"role", filterValue:"ADMIN" }) → total admin
├─ GET /api/whitelist?limit=1 → pagination.total
└─ GET /api/whitelist?limit=1 → pagination.total (§4.1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restore the missing third dashboard metric.

The sitemap repeats /api/whitelist?limit=1 twice, so this only defines two distinct data sources. Replace the duplicate with the third stat source intended for the dashboard.

🤖 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 `@plans/20260708-superadmin-real-api-integration/plan.md` around lines 83 - 86,
The dashboard metrics list currently duplicates GET /api/whitelist?limit=1,
leaving only two distinct data sources. Update the metric mapping in the plan so
the repeated whitelist entry is replaced with the intended third stat source,
keeping authClient.admin.listUsers({ filterField:"role", filterValue:"ADMIN" })
and pagination.total entries unique and adding the missing dashboard metric
alongside them.

Comment on lines +109 to +110
> **Lecturer requests:** Không có trong `00_auth.md` → **không** thêm nav/trang Phase 5.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
file='plans/20260708-superadmin-real-api-integration/plan.md'
wc -l "$file"
printf '\n--- 100-120 ---\n'
sed -n '100,120p' "$file"
printf '\n--- 190-205 ---\n'
sed -n '190,205p' "$file"
printf '\n--- 235-245 ---\n'
sed -n '235,245p' "$file"

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 1999


Unify the Phase 5 lecturer-requests decision. Around lines 109-110, 196-202, and 240-241, the plan says not to add the nav/page, then adds Phase 5 — Lecturer Requests, then cancels it. Collapse this into one explicit decision so the checklist has a single source of truth.

🤖 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 `@plans/20260708-superadmin-real-api-integration/plan.md` around lines 109 -
110, The Phase 5 lecturer-requests decision is duplicated and contradictory
across the plan, so consolidate it into one clear source of truth. Update the
Phase 5 section and the checklist in the plan so the decision about whether to
add the lecturer-requests nav/page is stated only once, and make sure the
references around the Phase 5 heading and the lecturer-requests bullet all match
that single decision.

Comment thread web/app/login/page.tsx
Comment on lines +74 to 86
const handleGoogleLogin = async () => {
setIsLoading(true);
setErrorMsg("");
try {
const success = await loginAsRole(role);
if (success) {
if (role === "superadmin") router.push("/superadmin");
else if (role === "teacher") router.push("/teacher");
else router.push("/student");
}
} catch (err) {
setErrorMsg("Đã xảy ra lỗi. Vui lòng thử lại.");
} finally {
await authClient.signIn.social({
provider: "google",
callbackURL: `${window.location.origin}/login`,
});
} catch {
setErrorMsg("Đã xảy ra lỗi khi đăng nhập Google.");
setIsLoading(false);
setShowRoleModal(false);
}
};

const handleGoogleLogin = async (role: UserRole) => {
setIsLoading(true);
setErrorMsg("");
try {
// Demo login using a default email
const demoEmail = role === "teacher" ? "lecturer1@fpt.edu.vn" : "student1@fpt.edu.vn";
const success = await loginWithGoogle(demoEmail, role);
if (success) {
if (role === "teacher") router.push("/teacher");
else router.push("/student");
}
} catch (err) {
setErrorMsg("Đã xảy ra lỗi. Vui lòng thử lại.");
} finally {
setIsLoading(false);
setShowRoleModal(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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and search for auth/session redirect logic.
git ls-files web/app/login/page.tsx web/app -z | tr '\0' '\n'
echo '--- outline page.tsx ---'
ast-grep outline web/app/login/page.tsx --view expanded || true
echo '--- search redirect/session refs ---'
rg -n "redirectAfterAuth|getSession|session|signIn\.social|callbackURL|router\.push|useEffect" web/app/login/page.tsx web/app -g '!**/node_modules/**' || true

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 16917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the login page and any helper used for post-auth routing.
for f in web/app/login/page.tsx web/app/**/redirectAfterAuth* web/app/**/auth*; do
  [ -f "$f" ] && echo "### $f" && wc -l "$f"
done

echo '--- page.tsx lines 1-220 ---'
sed -n '1,220p' web/app/login/page.tsx

echo '--- candidate auth redirect helpers ---'
rg -n "function redirectAfterAuth|const redirectAfterAuth|export .*redirectAfterAuth|redirectAfterAuth" web/app -g '!**/node_modules/**' || true

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 6810


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the auth redirect implementation and related login/portal routes.
rg -n "redirectAfterAuth|portal|login" web/app -g '!**/node_modules/**' || true

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 2660


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- web/app/login/page.tsx lines 220-265 ---'
sed -n '220,265p' web/app/login/page.tsx

echo '--- web/app/contexts/AuthContext.tsx outline ---'
ast-grep outline web/app/contexts/AuthContext.tsx --view expanded || true

echo '--- web/app/contexts/AuthContext.tsx lines 1-180 ---'
sed -n '1,180p' web/app/contexts/AuthContext.tsx

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 5401


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the login page tail for any mount-time redirect and the auth context for session side effects.
sed -n '220,265p' web/app/login/page.tsx
echo '---'
sed -n '1,180p' web/app/contexts/AuthContext.tsx

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 4371


Google OAuth still lands on /login without a redirect. callbackURL returns users to the login page, but this component never routes an authenticated session to the correct portal, so successful Google sign-ins can leave users stranded here. Add a mount-time redirect or point the callback at a route that redirects by role.

🤖 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 `@web/app/login/page.tsx` around lines 74 - 86, The Google sign-in flow in
handleGoogleLogin currently sends users back to the login page via callbackURL,
but LoginPage never redirects authenticated users onward. Update the login page
component to detect an existing session on mount and route users to the correct
portal, or change authClient.signIn.social’s callbackURL to a dedicated
post-auth route that performs the role-based redirect. Use the LoginPage
component and handleGoogleLogin as the main points to fix.

Comment on lines +295 to +299
const res = await authClient.admin.impersonateUser({ userId });
if (res.error) throw new Error(res.error.message);
refetchSession();
const role = (detail?.role as UserRole) ?? "STUDENT";
router.push(portalPathForRole(role));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target page component and nearby symbols
ast-grep outline 'web/app/superadmin/admins/[id]/page.tsx' --view expanded || true

# Read the relevant slice around the reported lines
sed -n '250,340p' 'web/app/superadmin/admins/[id]/page.tsx'

# Find refetchSession definitions/usages
rg -n "refetchSession|AuthContext|ProtectedRoute|portalPathForRole" web -S

# Inspect likely auth/session implementation files if found by ripgrep

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 6528


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the auth context/provider
sed -n '1,220p' 'web/app/contexts/AuthContext.tsx'

# Read the protected route implementation
sed -n '1,220p' 'web/components/ProtectedRoute.tsx'

# Read the relevant portion of the admin detail page with the impersonation handler context
sed -n '260,310p' 'web/app/superadmin/admins/[id]/page.tsx'

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 5735


Await the session refresh before pushing the portal. refetchSession() is fire-and-forget here (AuthContext wraps refetch() in void), so the impersonation flow can navigate while ProtectedRoute still sees the admin session and redirects back. Return the refetch promise from refetchSession (or call refetch() directly here) and await it before router.push(...).

🤖 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 `@web/app/superadmin/admins/`[id]/page.tsx around lines 295 - 299, The
impersonation flow in the admin page navigates before the auth session refresh
completes because refetchSession is fire-and-forget via AuthContext. Update the
impersonation handler in the page component to await the session refresh promise
before calling router.push, either by returning the underlying refetch() from
refetchSession or invoking refetch() directly here, so ProtectedRoute sees the
updated session before redirecting.

Comment on lines +91 to +129
const loadUsers = useCallback(async () => {
setLoading(true);
try {
const searchField = searchTerm.includes("@") ? "email" : "name";
const baseQuery: Record<string, string> = {
limit: roleFilter === "ALL" ? String(PAGE_SIZE) : "500",
offset: roleFilter === "ALL" ? String((page - 1) * PAGE_SIZE) : "0",
};
if (searchTerm) {
baseQuery.searchValue = searchTerm;
baseQuery.searchField = searchField;
}

const res = await authClient.admin.listUsers({ query: baseQuery });
if (res.error) {
throw new Error(res.error.message ?? "Không tải được danh sách người dùng");
}

let list = (res.data?.users ?? []) as AdminListUser[];
let count = res.data?.total ?? list.length;

if (roleFilter !== "ALL") {
list = list.filter((u) => u.role === roleFilter);
count = list.length;
const start = (page - 1) * PAGE_SIZE;
list = list.slice(start, start + PAGE_SIZE);
}

setUsers(list);
setTotal(count);
} catch (err) {
const message = err instanceof Error ? err.message : "Lỗi tải danh sách";
notifications.show({ title: "Lỗi", message, color: "red" });
setUsers([]);
setTotal(0);
} finally {
setLoading(false);
}
}, [page, searchTerm, roleFilter]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Role-filtered paging still truncates at 500 users

When roleFilter !== "ALL", this path loads only the first 500 users, filters them client-side, and paginates that slice. Any matching users beyond that window will never appear, and every page change re-fetches the same batch. Use server-side role filtering/pagination here instead of fetching a fixed 500.

🤖 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 `@web/app/superadmin/admins/page.tsx` around lines 91 - 129, The loadUsers
callback in admins/page.tsx is still doing client-side role filtering after
fetching a fixed 500-user batch, so pages beyond that window never surface all
matching admins. Update the authClient.admin.listUsers query path to apply
roleFilter server-side (or via an equivalent API-supported filter) and keep
pagination consistent with page/PAGE_SIZE instead of slicing the in-memory list
for non-ALL roles. Make sure the searchTerm flow and the existing loadUsers
callback continue to use the filtered query while preserving the current error
handling and setUsers/setTotal updates.

Comment on lines +83 to +96
const [usersPageRes, whitelistRes, usersListRes] = await Promise.all([
authClient.admin.listUsers({ query: { limit: "1", offset: "0" } }),
apiFetch<WhitelistListResponse>("/api/whitelist?page=1&limit=1"),
authClient.admin.listUsers({ query: { limit: "500", offset: "0" } }),
]);

if (usersPageRes.error) {
throw new Error(usersPageRes.error.message ?? "Không tải được danh sách người dùng");
}

const totalUsers = usersPageRes.data?.total ?? 0;
const users = usersListRes.data?.users ?? [];
const totalAdmins = users.filter((u) => u.role === "ADMIN").length;
const totalWhitelist = whitelistRes.pagination.total;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file around the referenced lines.
sed -n '1,180p' web/app/superadmin/page.tsx

printf '\n--- SEARCH listUsers USAGE ---\n'
rg -n "listUsers\\(|filterField|filterValue|filterOperator|role === \"ADMIN\"|totalAdmins" web -S

printf '\n--- FILE LIST FOR AUTH CLIENT REFERENCES ---\n'
rg -n "authClient" web -S

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 8678


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Try to find local docs or type definitions for the auth client.
rg -n "filterField|filterValue|filterOperator|listUsers" . -S --glob '!**/node_modules/**' --glob '!**/.next/**' --glob '!**/dist/**'

printf '\n--- candidate package metadata ---\n'
git ls-files | rg 'package\.json$|README|docs|auth|better' -n

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 4725


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect local auth docs and any Better Auth references that describe listUsers behavior.
sed -n '1,220p' docs/technical/backend/better_auth_guide.md

printf '\n---\n'
rg -n "filterField|filterValue|filterOperator|listUsers|total" docs api web -S --glob '!**/node_modules/**' --glob '!**/.next/**' --glob '!**/dist/**'

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 13136


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the auth client wiring and any API-side auth module for listUsers-related behavior.
sed -n '1,220p' web/lib/auth-client.ts

printf '\n--- API auth module ---\n'
sed -n '1,240p' api/src/modules/auth/auth.ts

printf '\n--- Better auth test script if present ---\n'
sed -n '1,220p' api/src/scripts/test-better-auth.ts

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 7905


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the auth API docs around listUsers/admin filtering.
sed -n '340,560p' docs/api/00_auth.md

printf '\n--- search for admin listUsers examples ---\n'
rg -n "filterField|filterValue|filterOperator|listUsers|total admin|role" docs/api/00_auth.md docs/technical/backend/better_auth_guide.md plans/20260708-superadmin-real-api-integration/plan.md -S

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 11740


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the admin user management page for any existing count pattern and error handling.
sed -n '1,220p' web/app/superadmin/admins/page.tsx

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 6255


Admin count should not be capped at 500 users

  • totalAdmins is derived from listUsers({ limit: "500" }), so the dashboard will undercount once the user base grows past 500.
  • Handle the second listUsers error explicitly; right now it falls back to 0 without surfacing the failure.
🤖 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 `@web/app/superadmin/page.tsx` around lines 83 - 96, The dashboard admin count
is being computed from a capped `authClient.admin.listUsers({ limit: "500" })`
result, so `totalAdmins` can undercount as the user base grows. Update the
`Promise.all` flow in `SuperadminPage` to either fetch all users or use a
dedicated count-based approach for admins, and do not rely on the 500-item page
for `usersListRes`. Also handle `usersListRes.error` explicitly, similar to
`usersPageRes.error`, instead of silently defaulting `users` to an empty array.

Comment on lines 311 to 318
<TextInput
placeholder="Tìm kiếm email sinh viên..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
leftSection={<IconSearch size={16} color="#9CA3AF" />}
radius={0}
style={{ maxWidth: "400px" }}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add an accessible name to the search input.

The TextInput relies solely on placeholder, which is not a reliable accessible name for screen readers. Add aria-label (or a visually-hidden label).

🛡️ Suggested fix
         <TextInput
           placeholder="Tìm kiếm email sinh viên..."
+          aria-label="Tìm kiếm email sinh viên"
           value={searchInput}
           onChange={(e) => setSearchInput(e.target.value)}
📝 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
<TextInput
placeholder="Tìm kiếm email sinh viên..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
leftSection={<IconSearch size={16} color="#9CA3AF" />}
radius={0}
style={{ maxWidth: "400px" }}
/>
<TextInput
placeholder="Tìm kiếm email sinh viên..."
aria-label="Tìm kiếm email sinh viên"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
leftSection={<IconSearch size={16} color="`#9CA3AF`" />}
radius={0}
style={{ maxWidth: "400px" }}
/>
🤖 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 `@web/app/superadmin/whitelist/page.tsx` around lines 311 - 318, The search
field in the superadmin whitelist page lacks an accessible name because the
TextInput only uses placeholder text. Update the TextInput used in the whitelist
search UI to include an explicit accessible name, either by adding an aria-label
or by providing a visually hidden label, so screen readers can identify it
reliably.

Comment thread web/lib/auth-client.ts
Comment on lines +39 to +46
res = await fetch(`${apiBaseUrl}${path}`, {
credentials: "include",
headers: {
"Content-Type": "application/json",
...init?.headers,
},
...init,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts -C4 '\bapiFetch\s*<' web

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 5614


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== web/lib/auth-client.ts =="
cat -n web/lib/auth-client.ts | sed -n '1,120p'

echo
echo "== apiFetch call sites with headers/credentials =="
rg -n -C3 'apiFetch\s*<|apiFetch\s*\(' web/app web/lib | sed -n '/apiFetch/,$p' | head -n 200
echo
echo "== explicit headers/credentials near apiFetch =="
rg -n -C2 'apiFetch\s*\(.*\{|headers\s*:|credentials\s*:' web/app web/lib

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 10719


Spread init before the defaults. ...init currently overwrites credentials: "include" and the merged headers object if a caller passes either field. Move the spread earlier, or set headers after ...init so this helper keeps its defaults.

🤖 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 `@web/lib/auth-client.ts` around lines 39 - 46, The fetch options in
auth-client’s request helper are spreading init after the default settings,
which allows caller-provided credentials and headers to overwrite the helper’s
defaults. Update the request construction in the fetch wrapper so the init
spread happens before the default credentials and merged headers, or assign the
defaults after init, ensuring credentials: include and the JSON
Content-Type/header merge always remain in effect.

@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.

🧹 Nitpick comments (1)
api/src/config/supabase.ts (1)

8-13: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Set persistSession: false on the server-side anon client.

createSupabaseClient() is a server-side factory but does not disable session persistence. The service client correctly sets persistSession: false. Without it, the Supabase client may attempt to persist sessions in memory, leading to leaks and unexpected behavior in server contexts.

♻️ Proposed fix
 export function createSupabaseClient(): SupabaseClient {
   if (!supabaseUrl || !supabaseAnonKey) {
     throw new Error("Missing Supabase URL or anon key");
   }
-  return createClient(supabaseUrl, supabaseAnonKey);
+  return createClient(supabaseUrl, supabaseAnonKey, {
+    auth: { persistSession: false, autoRefreshToken: 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 `@api/src/config/supabase.ts` around lines 8 - 13, The server-side Supabase
factory in createSupabaseClient() is missing the anon client option that
disables session persistence. Update the createClient call to pass an
auth/config setting with persistSession disabled, matching the service client
behavior, so the anon client does not try to retain sessions in server contexts.
🤖 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.

Nitpick comments:
In `@api/src/config/supabase.ts`:
- Around line 8-13: The server-side Supabase factory in createSupabaseClient()
is missing the anon client option that disables session persistence. Update the
createClient call to pass an auth/config setting with persistSession disabled,
matching the service client behavior, so the anon client does not try to retain
sessions in server contexts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfb7af09-c9fc-492e-9a1d-7036dcc889b4

📥 Commits

Reviewing files that changed from the base of the PR and between 6331c9e and ff7e546.

📒 Files selected for processing (2)
  • api/src/config/supabase.ts
  • web/lib/supabase-client.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: 14

🧹 Nitpick comments (2)
web/app/teacher/documents/page.tsx (1)

339-354: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dropzone doesn't enforce PDF-only — validation happens too late.

The Dropzone accepts any file type. Users can drop a non-PDF file, see it listed, click "Bắt đầu upload", and only then receive an error. Add an accept prop to reject non-PDF files at drop time, providing immediate feedback.

♻️ Proposed fix: add accept prop to Dropzone
           <Dropzone
             onDrop={(acceptedFiles) => setFiles(acceptedFiles)}
             maxSize={50 * 1024 ** 2}
+            accept={{ "application/pdf": [".pdf"] }}
             radius={0}
🤖 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 `@web/app/teacher/documents/page.tsx` around lines 339 - 354, Configure the
Dropzone component in the documents page to accept only PDF files by adding its
accept prop with the appropriate PDF MIME type. Keep the existing onDrop handler
and size limit unchanged so non-PDF files are rejected immediately during
selection or drop.
web/lib/api.ts (1)

259-285: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

uploadSyllabusDocument duplicates error-handling logic from fetchApi.

The manual fetch + error-parsing block is a near-verbatim copy of fetchApi lines 37–46. Consider extending fetchApi to skip Content-Type: application/json when the body is FormData, then routing through it to eliminate the duplication.

♻️ Proposed refactor: extend fetchApi to support FormData
 async function fetchApi<T>(
   path: string,
   options: RequestInit = {}
 ): Promise<T> {
   const url = `${API_BASE_URL}${path}`;
+  const isFormData = options.body instanceof FormData;
   const res = await fetch(url, {
     ...options,
     credentials: "include",
     headers: {
-      "Content-Type": "application/json",
+      ...(isFormData ? {} : { "Content-Type": "application/json" }),
       ...options.headers,
     },
   });

Then uploadSyllabusDocument can simply call fetchApi:

 export async function uploadSyllabusDocument(
   syllabusId: number,
   file: File
 ): Promise<{ document: ApiDocument }> {
-  const url = `${API_BASE_URL}/api/syllabus/${syllabusId}/documents`;
   const formData = new FormData();
   formData.append("file", file);
-
-  const res = await fetch(url, {
-    method: "POST",
-    credentials: "include",
-    body: formData,
-  });
-
-  if (!res.ok) {
-    let errorMessage = `API Error: ${res.status}`;
-    try {
-      const body = await res.json();
-      errorMessage = body.error || errorMessage;
-    } catch {
-      // ignore parse errors
-    }
-    throw new ApiError(errorMessage, res.status);
-  }
-
-  return res.json();
+  return fetchApi(`/api/syllabus/${syllabusId}/documents`, {
+    method: "POST",
+    body: formData,
+  });
 }
🤖 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 `@web/lib/api.ts` around lines 259 - 285, Refactor uploadSyllabusDocument to
use the shared fetchApi helper instead of duplicating fetch and error parsing.
Update fetchApi to omit the application/json Content-Type header when its
request body is FormData, while preserving JSON handling for other bodies, then
pass the FormData request through fetchApi and return its parsed response.
🤖 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 `@api/src/modules/rag/services/rag.service.ts`:
- Around line 188-194: In the Gemini fallback catch block of RagService, keep
logging geminiError server-side but remove its message from the client-facing
errorReply. Return only the generic apology through onChunk and fullAnswer,
without exposing provider or internal error details.

In `@credentials.json`:
- Line 6: Remove plaintext passwords and user IDs from tracked credentials.json;
move seeded credentials to an ignored local fixture or generate them through
environment-backed test fixtures, ensuring student1@fpt.edu.vn does not use the
trivial password and adding the relevant file to gitignore.

In `@web/app/student/syllabus/`[subjectCode]/page.tsx:
- Around line 117-119: Remove the "FER202" fallback in SyllabusViewerPage and
preserve an undefined or empty subjectCode when params.subjectCode is missing;
update loadSyllabus to handle the missing code explicitly or render an
appropriate error state before loading syllabus data.
- Around line 192-225: Update the not-found rendering condition in the syllabus
page so API failures display a primary backend/server error message instead of
“Không tìm thấy Syllabus.” Within the error block, use error === "API_ERROR" to
select the appropriate title and supporting text while preserving the existing
not-found messaging for genuine missing syllabi.

In `@web/app/teacher/documents/page.tsx`:
- Around line 141-152: handleDelete currently calls api.deleteSyllabusDocument
without user confirmation, allowing accidental permanent deletion. Add a
confirmation step at the start of handleDelete, using the existing UI
conventions or window.confirm, and return early when the user cancels; only call
api.deleteSyllabusDocument and reload via loadDocuments after confirmation.

In `@web/app/teacher/syllabus/page.tsx`:
- Line 65: Replace the incorrect top-level syllabus.code references in
filteredSyllabi and the Subject Code table cell with syllabus.course.code,
matching the ApiSyllabusSummary shape and existing course.name/course.code
usage.

In `@web/components/chatbot/ChatbotWidget.tsx`:
- Around line 141-151: The onError handler in ChatbotWidget must always expose
failures, including when partial bot content already exists. Update the message
mapping for botMsgId so it preserves existing m.content and appends a clear
error indicator containing error, rather than using the fallback-only expression
m.content || `⚠️ Lỗi: ${error}`.
- Around line 85-177: Add a useEffect cleanup for the ChatbotWidget component
that aborts any active request in abortRef.current and clears the ref on
unmount. Ensure this cleanup covers streams started by the sendChatMessageStream
flow and prevents callbacks from continuing after the component is removed.
- Around line 100-108: Prevent the empty bot bubble and loading indicator from
rendering together by deferring insertion of the empty bot message in the
streaming handler until the first onChunk callback, then append or update it
with the received content; update the related state logic so subsequent chunks
continue targeting the same bot message ID.

In `@web/components/chatbot/subcomponents/ChatHistorySidebar.tsx`:
- Around line 45-62: Update ChatHistorySidebar’s session-selection flow to
delegate to an abort-aware onSessionSelect callback supplied by ChatbotWidget,
mirroring handleNewChat so any active stream is aborted and loading state is
reset before switching sessions. Ensure the callback replaces the local
handleSessionClick logic, and when the selected session has no messages,
explicitly clear the messages state before setting the new session ID.

In `@web/components/student/syllabus/SyllabusHelpers.tsx`:
- Around line 16-24: Remove the description-based getbootstrap.com/react.dev
conditional from isMaterialChecked, including its forced true handling for
is_main_material and is_online. Keep only the actual field-value checks and
return false when those values are not truthy.

In `@web/components/student/syllabus/SyllabusScheduleTab.tsx`:
- Around line 95-109: Replace the hardcoded FER202/PRN232 check in the syllabus
schedule rendering with a generalized course-code validation, such as matching
the API’s supported three-letter, three-digit format, or use a provided download
URL when available. Keep valid codes rendered as download Anchors with the
existing URL structure and render other values with formatMultiline.

In `@web/lib/api.ts`:
- Around line 369-405: Fix the SSE parsing in the reader loop by normalizing or
trimming trailing carriage returns from lines before checking prefixes and
parsing JSON, so CRLF-delimited events are handled correctly. After the read
loop exits, process any remaining non-empty buffer as a final SSE line, applying
the same data parsing and callbacks logic used inside the loop before invoking
callbacks.onDone().

In `@web/next.config.ts`:
- Around line 6-24: Update the top-level environment-loading try/catch in
next.config.ts to surface failures from process.loadEnvFile, including
unsupported Node versions and malformed .env files. Replace the empty catch with
a clear console error or rethrow that includes the captured error, while
preserving the existing upward .env search behavior.

---

Nitpick comments:
In `@web/app/teacher/documents/page.tsx`:
- Around line 339-354: Configure the Dropzone component in the documents page to
accept only PDF files by adding its accept prop with the appropriate PDF MIME
type. Keep the existing onDrop handler and size limit unchanged so non-PDF files
are rejected immediately during selection or drop.

In `@web/lib/api.ts`:
- Around line 259-285: Refactor uploadSyllabusDocument to use the shared
fetchApi helper instead of duplicating fetch and error parsing. Update fetchApi
to omit the application/json Content-Type header when its request body is
FormData, while preserving JSON handling for other bodies, then pass the
FormData request through fetchApi and return its parsed response.
🪄 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: c53766de-aabe-4e04-9db6-f287d7764c64

📥 Commits

Reviewing files that changed from the base of the PR and between ff7e546 and e94937e.

📒 Files selected for processing (27)
  • api/prisma/seed.ts
  • api/src/modules/rag/services/rag.service.ts
  • credentials.json
  • web/app/contexts/AuthContext.tsx
  • web/app/login/page.tsx
  • web/app/student/layout.tsx
  • web/app/student/page.tsx
  • web/app/student/syllabus/[subjectCode]/page.tsx
  • web/app/teacher/documents/page.tsx
  • web/app/teacher/syllabus/page.tsx
  • web/components/ProtectedRoute.tsx
  • web/components/chatbot/ChatbotWidget.tsx
  • web/components/chatbot/subcomponents/ChatHistorySidebar.tsx
  • web/components/chatbot/subcomponents/ChatInputArea.tsx
  • web/components/chatbot/subcomponents/ChatMessageList.tsx
  • web/components/chatbot/subcomponents/types.ts
  • web/components/student/SearchResultRow.tsx
  • web/components/student/SubjectCard.tsx
  • web/components/student/subjectsData.ts
  • web/components/student/syllabus/SyllabusAssessmentTab.tsx
  • web/components/student/syllabus/SyllabusCLOsTab.tsx
  • web/components/student/syllabus/SyllabusHelpers.tsx
  • web/components/student/syllabus/SyllabusMaterialsTab.tsx
  • web/components/student/syllabus/SyllabusOverviewTab.tsx
  • web/components/student/syllabus/SyllabusScheduleTab.tsx
  • web/lib/api.ts
  • web/next.config.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/components/ProtectedRoute.tsx
  • web/app/login/page.tsx

Comment on lines +188 to +194
} catch (geminiError) {
console.error("[RagService] Gemini fallback also failed:", geminiError);
const errMsg = geminiError instanceof Error ? geminiError.message : String(geminiError);
const errorReply = `⚠️ Xin lỗi, hiện tại AI trợ lý đang gặp sự cố kỹ thuật. Vui lòng thử lại sau.\n\nChi tiết lỗi: ${errMsg}`;
onChunk(errorReply);
return { citations: [], fullAnswer: errorReply };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file lines =="
nl -ba api/src/modules/rag/services/rag.service.ts | sed -n '150,210p'

echo
echo "== onChunk references in rag service =="
rg -n "onChunk|errorReply|Gemini fallback|AnythingLLM" api/src/modules/rag/services/rag.service.ts

echo
echo "== call sites / definition of onChunk if present in module tree =="
rg -n "onChunk\s*[:(]|type .*onChunk|interface .*onChunk|function .*onChunk" api/src/modules/rag -g '*.ts'

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('api/src/modules/rag/services/rag.service.ts')
lines = p.read_text().splitlines()
for i in range(150, 211):
    if i <= len(lines):
        print(f"{i:4d}: {lines[i-1]}")
PY

echo
echo "== onChunk / error handling matches =="
python3 - <<'PY'
from pathlib import Path
import re
p = Path('api/src/modules/rag/services/rag.service.ts')
text = p.read_text()
for pat in [r'onChunk', r'errorReply', r'Gemini fallback', r'AnythingLLM']:
    print(f'-- {pat} --')
    for i, line in enumerate(text.splitlines(), 1):
        if re.search(pat, line):
            print(f"{i}: {line}")
PY

echo
echo "== module-wide onChunk references =="
rg -n "onChunk|errorReply|Gemini fallback|AnythingLLM" api/src/modules/rag -g '*.ts'

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 7606


Don't stream raw Gemini error details to clients. geminiError.message is concatenated into the user-facing reply and sent through onChunk, which can expose internal provider details. Keep the server-side log, but return only the generic apology here. api/src/modules/rag/services/rag.service.ts:188-193

🤖 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 `@api/src/modules/rag/services/rag.service.ts` around lines 188 - 194, In the
Gemini fallback catch block of RagService, keep logging geminiError server-side
but remove its message from the client-facing errorReply. Return only the
generic apology through onChunk and fullAnswer, without exposing provider or
internal error details.

Comment thread credentials.json
"name": "Sinh vien Mot",
"email": "student1@fpt.edu.vn",
"password": "StudentPassword123!",
"password": "123456",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether credentials.json is git-ignored and where it is consumed
fd -H -t f 'credentials.json'
rg -n 'credentials\.json' -g '!credentials.json'
git check-ignore credentials.json || echo "NOT ignored by git"

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file locations =="
fd -H -t f 'credentials.json' .

echo
echo "== top-level gitignore entries =="
sed -n '1,200p' .gitignore 2>/dev/null || true

echo
echo "== credentials.json contents with line numbers =="
cat -n credentials.json

echo
echo "== references to credentials.json =="
rg -n --hidden --glob '!credentials.json' 'credentials\.json|StudentPassword123!|123456|fpt\.edu\.vn' .

echo
echo "== git ignore status =="
git check-ignore -v credentials.json || echo "NOT ignored by git"

echo
echo "== tracked status =="
git ls-files --stage -- credentials.json || echo "NOT tracked"

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 14114


Committed plaintext credentials. credentials.json:6 is tracked in git and not ignored, so the seeded passwords and user IDs live in the repo. student1@fpt.edu.vn also now uses the trivial 123456 password. Move this to ignored local data or generate it from environment-backed fixtures instead.

🤖 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 `@credentials.json` at line 6, Remove plaintext passwords and user IDs from
tracked credentials.json; move seeded credentials to an ignored local fixture or
generate them through environment-backed test fixtures, ensuring
student1@fpt.edu.vn does not use the trivial password and adding the relevant
file to gitignore.

Comment on lines 117 to 119
export default function SyllabusViewerPage() {
const params = useParams();
const subjectCode = (params.subjectCode as string)?.toUpperCase() || "FER202";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove hardcoded "FER202" fallback for missing subject code.

If params.subjectCode is undefined (e.g., navigated to /student/syllabus/ without a code), the page silently loads FER202 data instead of showing an error. This is confusing in production.

🛡️ Proposed fix
-  const subjectCode = (params.subjectCode as string)?.toUpperCase() || "FER202";
+  const rawCode = params.subjectCode as string | undefined;
+  const subjectCode = rawCode?.toUpperCase() ?? "";

Then handle the empty-string case in the loadSyllabus callback or render an error early.

📝 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 default function SyllabusViewerPage() {
const params = useParams();
const subjectCode = (params.subjectCode as string)?.toUpperCase() || "FER202";
export default function SyllabusViewerPage() {
const params = useParams();
const rawCode = params.subjectCode as string | undefined;
const subjectCode = rawCode?.toUpperCase() ?? "";
🤖 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 `@web/app/student/syllabus/`[subjectCode]/page.tsx around lines 117 - 119,
Remove the "FER202" fallback in SyllabusViewerPage and preserve an undefined or
empty subjectCode when params.subjectCode is missing; update loadSyllabus to
handle the missing code explicitly or render an appropriate error state before
loading syllabus data.

Comment on lines +192 to 225
// Not found
if (error === "NOT_FOUND" || !syllabusData) {
return (
<Container size="sm" py="xl" style={{ textAlign: "center" }}>
<Card p="xl" radius="lg" style={{ border: "1px dashed #fca5a5", background: "#fff5f5" }}>
<Stack align="center" gap="md">
<ThemeIcon size={64} radius="xl" style={{ background: "#fee2e2", color: "#dc2626" }}>
<IconX size={32} />
</ThemeIcon>
<Text fw={800} style={{ color: "#dc2626", fontSize: "18px" }}>
Không tìm thấy Syllabus
</Text>
<Text c="dimmed" size="sm">
Môn học <strong>{subjectCode}</strong> chưa có trong hệ thống hoặc chưa được duyệt.
</Text>
{error === "API_ERROR" && (
<Text c="dimmed" size="xs">
<IconAlertCircle size={12} style={{ verticalAlign: "middle" }} /> Không thể kết nối đến server backend.
</Text>
)}
<Button
component={Link}
href="/student"
radius="md"
style={{ background: "linear-gradient(135deg, #1A3A5C, #0f2848)" }}
leftSection={<IconArrowLeft size={14} />}
>
Quay lại tìm kiếm
</Button>
</Stack>
</Card>
</Container>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

API errors show a misleading "Not found" primary message.

When error === "API_ERROR", syllabusData is null, so this block renders. The primary text says "Không tìm thấy Syllabus" (Not found) even though the real issue is a backend connection failure. The API error subtext at line 207-211 is shown conditionally, but the primary message is still wrong.

🛡️ Proposed fix
-    <Text fw={800} style={{ color: "`#dc2626`", fontSize: "18px" }}>
-      Không tìm thấy Syllabus
-    </Text>
-    <Text c="dimmed" size="sm">
-      Môn học <strong>{subjectCode}</strong> chưa có trong hệ thống hoặc chưa được duyệt.
-    </Text>
-    {error === "API_ERROR" && (
-      <Text c="dimmed" size="xs">
-        <IconAlertCircle size={12} style={{ verticalAlign: "middle" }} /> Không thể kết nối đến server backend.
-      </Text>
-    )}
+    <Text fw={800} style={{ color: "`#dc2626`", fontSize: "18px" }}>
+      {error === "API_ERROR" ? "Lỗi kết nối" : "Không tìm thấy Syllabus"}
+    </Text>
+    <Text c="dimmed" size="sm">
+      {error === "API_ERROR"
+        ? "Không thể kết nối đến server backend. Vui lòng thử lại sau."
+        : <>Môn học <strong>{subjectCode}</strong> chưa có trong hệ thống hoặc chưa được duyệt.</>
+      }
+    </Text>
📝 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
// Not found
if (error === "NOT_FOUND" || !syllabusData) {
return (
<Container size="sm" py="xl" style={{ textAlign: "center" }}>
<Card p="xl" radius="lg" style={{ border: "1px dashed #fca5a5", background: "#fff5f5" }}>
<Stack align="center" gap="md">
<ThemeIcon size={64} radius="xl" style={{ background: "#fee2e2", color: "#dc2626" }}>
<IconX size={32} />
</ThemeIcon>
<Text fw={800} style={{ color: "#dc2626", fontSize: "18px" }}>
Không tìm thấy Syllabus
</Text>
<Text c="dimmed" size="sm">
Môn học <strong>{subjectCode}</strong> chưa có trong hệ thống hoặc chưa được duyệt.
</Text>
{error === "API_ERROR" && (
<Text c="dimmed" size="xs">
<IconAlertCircle size={12} style={{ verticalAlign: "middle" }} /> Không thể kết nối đến server backend.
</Text>
)}
<Button
component={Link}
href="/student"
radius="md"
style={{ background: "linear-gradient(135deg, #1A3A5C, #0f2848)" }}
leftSection={<IconArrowLeft size={14} />}
>
Quay lại tìm kiếm
</Button>
</Stack>
</Card>
</Container>
);
}
// Not found
if (error === "NOT_FOUND" || !syllabusData) {
return (
<Container size="sm" py="xl" style={{ textAlign: "center" }}>
<Card p="xl" radius="lg" style={{ border: "1px dashed `#fca5a5`", background: "`#fff5f5`" }}>
<Stack align="center" gap="md">
<ThemeIcon size={64} radius="xl" style={{ background: "`#fee2e2`", color: "`#dc2626`" }}>
<IconX size={32} />
</ThemeIcon>
<Text fw={800} style={{ color: "`#dc2626`", fontSize: "18px" }}>
{error === "API_ERROR" ? "Lỗi kết nối" : "Không tìm thấy Syllabus"}
</Text>
<Text c="dimmed" size="sm">
{error === "API_ERROR"
? "Không thể kết nối đến server backend. Vui lòng thử lại sau."
: <>Môn học <strong>{subjectCode}</strong> chưa có trong hệ thống hoặc chưa được duyệt.</>
}
</Text>
<Button
component={Link}
href="/student"
radius="md"
style={{ background: "linear-gradient(135deg, `#1A3A5C`, `#0f2848`)" }}
leftSection={<IconArrowLeft size={14} />}
>
Quay lại tìm kiếm
</Button>
</Stack>
</Card>
</Container>
);
}
🤖 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 `@web/app/student/syllabus/`[subjectCode]/page.tsx around lines 192 - 225,
Update the not-found rendering condition in the syllabus page so API failures
display a primary backend/server error message instead of “Không tìm thấy
Syllabus.” Within the error block, use error === "API_ERROR" to select the
appropriate title and supporting text while preserving the existing not-found
messaging for genuine missing syllabi.

Comment on lines +141 to +152
const handleDelete = async (documentId: string) => {
if (!selectedSyllabusId) return;
setApiError(null);

try {
await api.deleteSyllabusDocument(selectedSyllabusId, documentId);
void loadDocuments(selectedSyllabusId);
} catch (err) {
console.error("Delete failed:", err);
setApiError("Không thể xóa tài liệu.");
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Destructive delete has no confirmation dialog.

handleDelete permanently removes a document with a single click and no confirmation. An accidental click causes irreversible data loss. Add a confirmation dialog (e.g., Mantine Modal or window.confirm) before calling the API.

🛡️ Proposed fix: add confirmation before delete
   const handleDelete = async (documentId: string) => {
     if (!selectedSyllabusId) return;
+    if (!window.confirm("Bạn có chắc muốn xóa tài liệu này?")) return;
     setApiError(null);

Also applies to: 300-300

🤖 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 `@web/app/teacher/documents/page.tsx` around lines 141 - 152, handleDelete
currently calls api.deleteSyllabusDocument without user confirmation, allowing
accidental permanent deletion. Add a confirmation step at the start of
handleDelete, using the existing UI conventions or window.confirm, and return
early when the user cancels; only call api.deleteSyllabusDocument and reload via
loadDocuments after confirmation.

Comment on lines +45 to +62
const handleSessionClick = async (session: api.ApiChatSession) => {
try {
const { session: detail } = await api.getChatSessionDetail(session.id);
if (detail.messages && detail.messages.length > 0) {
const mapped: Message[] = detail.messages.map((m) => ({
id: m.id,
role: m.sender === "USER" ? "user" : "bot",
content: m.content,
timestamp: new Date(m.createdAt),
}));
setMessages(mapped);
}
setSessionId?.(session.id);
setIsHistoryOpen(false);
} catch {
// ignore
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

handleSessionClick doesn't abort in-flight streaming and doesn't handle empty sessions.

Two issues in this handler:

  1. No abort: When the user clicks a history session while a stream is in progress, the old stream's onDone callback will eventually set isLoading to false, but until then isLoading remains true, which blocks handleSend (line 72: if (!input.trim() || isLoading || ...) return). The user is stuck waiting for the old stream to finish. The parent's handleNewChat correctly aborts via abortRef, but handleSessionClick has no access to it.

  2. Empty messages: If detail.messages is empty or undefined, setMessages is skipped but setSessionId is still set to the new session. The user sees stale messages from the previous session under a new session ID.

Consider passing an onSessionSelect callback from ChatbotWidget that handles abort + state updates, similar to handleNewChat:

🔒 Suggested fix: pass abort-aware callback from parent

In ChatbotWidget.tsx:

+ const handleSessionSelect = useCallback(async (session: api.ApiChatSession) => {
+   if (abortRef.current) {
+     abortRef.current.abort();
+     abortRef.current = null;
+   }
+   setIsLoading(false);
+   try {
+     const { session: detail } = await api.getChatSessionDetail(session.id);
+     if (detail.messages && detail.messages.length > 0) {
+       setMessages(detail.messages.map((m) => ({
+         id: m.id,
+         role: m.sender === "USER" ? "user" : "bot",
+         content: m.content,
+         timestamp: new Date(m.createdAt),
+       })));
+     } else {
+       setMessages([{
+         id: Date.now().toString(),
+         role: "bot",
+         content: `Cuộc trò chuyện về ${subjectCode}.`,
+         timestamp: new Date(),
+       }]);
+     }
+     setSessionId(session.id);
+     setIsHistoryOpen(false);
+   } catch {
+     // handle error
+   }
+ }, [subjectCode]);

Then pass it to ChatHistorySidebar and use it instead of the local handleSessionClick.

🤖 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 `@web/components/chatbot/subcomponents/ChatHistorySidebar.tsx` around lines 45
- 62, Update ChatHistorySidebar’s session-selection flow to delegate to an
abort-aware onSessionSelect callback supplied by ChatbotWidget, mirroring
handleNewChat so any active stream is aborted and loading state is reset before
switching sessions. Ensure the callback replaces the local handleSessionClick
logic, and when the selected session has no messages, explicitly clear the
messages state before setting the new session ID.

Comment on lines +16 to +24
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isMaterialChecked = (m: any, field: string) => {
if (m[field] === "true" || m[field] === "checked" || m[field] === true || m[field] === "1")
return true;
if (m.description?.includes("getbootstrap.com") || m.description?.includes("react.dev")) {
if (field === "is_main_material" || field === "is_online") return true;
}
return 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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove hardcoded mock-data checks from isMaterialChecked.

Lines 20-22 force is_main_material and is_online to true when a material's description contains "getbootstrap.com" or "react.dev". This is leftover mock/test data logic. In production, any real material with those URLs in its description would incorrectly display as "main material" and "online" regardless of actual API values.

🛡️ Proposed fix
 export const isMaterialChecked = (m: any, field: string) => {
   if (m[field] === "true" || m[field] === "checked" || m[field] === true || m[field] === "1")
     return true;
-  if (m.description?.includes("getbootstrap.com") || m.description?.includes("react.dev")) {
-    if (field === "is_main_material" || field === "is_online") return true;
-  }
   return 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
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isMaterialChecked = (m: any, field: string) => {
if (m[field] === "true" || m[field] === "checked" || m[field] === true || m[field] === "1")
return true;
if (m.description?.includes("getbootstrap.com") || m.description?.includes("react.dev")) {
if (field === "is_main_material" || field === "is_online") return true;
}
return false;
};
// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
export const isMaterialChecked = (m: any, field: string) => {
if (m[field] === "true" || m[field] === "checked" || m[field] === true || m[field] === "1")
return true;
return 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 `@web/components/student/syllabus/SyllabusHelpers.tsx` around lines 16 - 24,
Remove the description-based getbootstrap.com/react.dev conditional from
isMaterialChecked, including its forced true handling for is_main_material and
is_online. Keep only the actual field-value checks and return false when those
values are not truthy.

Comment on lines +95 to +109
<Table.Td>
{s.s_download === "FER202" || s.s_download === "PRN232" ? (
<Anchor
href={`https://flm.fpt.edu.vn/download/${metadata.syllabus_id}/S/1_${s.s_download}.zip`}
size="xs"
style={{ color: "#1A3A5C" }}
>
<Group gap={4}>
<IconDownload size={12} />
{s.s_download}
</Group>
</Anchor>
) : (
<Text size="xs">{formatMultiline(s.s_download) || "—"}</Text>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Hardcoded course-specific download logic only works for FER202 and PRN232.

The condition s.s_download === "FER202" || s.s_download === "PRN232" generates download links only for two specific courses. All other courses render s_download as plain text via formatMultiline, even if they have valid download codes. This should use a more general pattern, such as checking if s_download matches a course-code format or if the API provides a download URL directly.

🛡️ Proposed fix
-                {s.s_download === "FER202" || s.s_download === "PRN232" ? (
+                {/^[A-Z]{3}\d{3}$/.test(s.s_download) ? (
                    <Anchor
                      href={`https://flm.fpt.edu.vn/download/${metadata.syllabus_id}/S/1_${s.s_download}.zip`}

This regex matches any FPT-style course code (3 letters + 3 digits). Adjust the pattern based on the actual course code format used by the API.

📝 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
<Table.Td>
{s.s_download === "FER202" || s.s_download === "PRN232" ? (
<Anchor
href={`https://flm.fpt.edu.vn/download/${metadata.syllabus_id}/S/1_${s.s_download}.zip`}
size="xs"
style={{ color: "#1A3A5C" }}
>
<Group gap={4}>
<IconDownload size={12} />
{s.s_download}
</Group>
</Anchor>
) : (
<Text size="xs">{formatMultiline(s.s_download) || "—"}</Text>
)}
<Table.Td>
{/^[A-Z]{3}\d{3}$/.test(s.s_download) ? (
<Anchor
href={`https://flm.fpt.edu.vn/download/${metadata.syllabus_id}/S/1_${s.s_download}.zip`}
size="xs"
style={{ color: "`#1A3A5C`" }}
>
<Group gap={4}>
<IconDownload size={12} />
{s.s_download}
</Group>
</Anchor>
) : (
<Text size="xs">{formatMultiline(s.s_download) || "—"}</Text>
)}
🤖 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 `@web/components/student/syllabus/SyllabusScheduleTab.tsx` around lines 95 -
109, Replace the hardcoded FER202/PRN232 check in the syllabus schedule
rendering with a generalized course-code validation, such as matching the API’s
supported three-letter, three-digit format, or use a provided download URL when
available. Keep valid codes rendered as download Anchors with the existing URL
structure and render other values with formatMultiline.

Comment thread web/lib/api.ts
Comment on lines +369 to +405
const decoder = new TextDecoder();
let buffer = "";

while (true) {
const { done, value } = await reader.read();
if (done) break;

buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";

for (const line of lines) {
if (line.startsWith("event: ")) {
// event type handled via next data line
continue;
}
if (line.startsWith("data: ")) {
const data = line.slice(6);
try {
const parsed = JSON.parse(data);
if (parsed.chunk) {
callbacks.onChunk(parsed.chunk);
}
if (parsed.citations) {
callbacks.onCitations(parsed.citations);
}
if (parsed.error) {
callbacks.onError(parsed.error);
}
} catch {
// ignore malformed lines
}
}
}
}

callbacks.onDone();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

SSE stream drops last data line and silently fails on \r\n line endings.

Two parsing defects in the SSE reader:

  1. After the while loop exits (done === true), buffer may still contain the final data: line that never ended with \n. This line is never processed, so the last chunk can be lost — producing truncated chat responses.

  2. buffer.split("\n") doesn't handle \r\n endings. Each line retains a trailing \r, causing JSON.parse(data) to throw, and the catch block silently drops the entire data line. If the server uses \r\n, all chunks are lost.

🔧 Proposed fix: process remaining buffer and handle `\r\n`
       const decoder = new TextDecoder();
       let buffer = "";

       while (true) {
         const { done, value } = await reader.read();
         if (done) break;

         buffer += decoder.decode(value, { stream: true });
-        const lines = buffer.split("\n");
+        const lines = buffer.split(/\r?\n/);
         buffer = lines.pop() || "";

         for (const line of lines) {
           if (line.startsWith("event: ")) {
             continue;
           }
           if (line.startsWith("data: ")) {
-            const data = line.slice(6);
+            const data = line.slice(6).trim();
             try {
               const parsed = JSON.parse(data);
               if (parsed.chunk) {
                 callbacks.onChunk(parsed.chunk);
               }
               if (parsed.citations) {
                 callbacks.onCitations(parsed.citations);
               }
               if (parsed.error) {
                 callbacks.onError(parsed.error);
               }
             } catch {
               // ignore malformed lines
             }
           }
         }
       }

+      // Process any remaining buffered line after stream ends
+      if (buffer.startsWith("data: ")) {
+        const data = buffer.slice(6).trim();
+        try {
+          const parsed = JSON.parse(data);
+          if (parsed.chunk) callbacks.onChunk(parsed.chunk);
+          if (parsed.citations) callbacks.onCitations(parsed.citations);
+          if (parsed.error) callbacks.onError(parsed.error);
+        } catch {
+          // ignore malformed final line
+        }
+      }
+
       callbacks.onDone();
📝 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 decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("event: ")) {
// event type handled via next data line
continue;
}
if (line.startsWith("data: ")) {
const data = line.slice(6);
try {
const parsed = JSON.parse(data);
if (parsed.chunk) {
callbacks.onChunk(parsed.chunk);
}
if (parsed.citations) {
callbacks.onCitations(parsed.citations);
}
if (parsed.error) {
callbacks.onError(parsed.error);
}
} catch {
// ignore malformed lines
}
}
}
}
callbacks.onDone();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split(/\r?\n/);
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("event: ")) {
// event type handled via next data line
continue;
}
if (line.startsWith("data: ")) {
const data = line.slice(6).trim();
try {
const parsed = JSON.parse(data);
if (parsed.chunk) {
callbacks.onChunk(parsed.chunk);
}
if (parsed.citations) {
callbacks.onCitations(parsed.citations);
}
if (parsed.error) {
callbacks.onError(parsed.error);
}
} catch {
// ignore malformed lines
}
}
}
}
// Process any remaining buffered line after stream ends
if (buffer.startsWith("data: ")) {
const data = buffer.slice(6).trim();
try {
const parsed = JSON.parse(data);
if (parsed.chunk) callbacks.onChunk(parsed.chunk);
if (parsed.citations) callbacks.onCitations(parsed.citations);
if (parsed.error) callbacks.onError(parsed.error);
} catch {
// ignore malformed final line
}
}
callbacks.onDone();
🤖 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 `@web/lib/api.ts` around lines 369 - 405, Fix the SSE parsing in the reader
loop by normalizing or trimming trailing carriage returns from lines before
checking prefixes and parsing JSON, so CRLF-delimited events are handled
correctly. After the read loop exits, process any remaining non-empty buffer as
a final SSE line, applying the same data parsing and callbacks logic used inside
the loop before invoking callbacks.onDone().

Comment thread web/next.config.ts
Comment on lines +6 to +24
try {
let dir = __dirname;
let envPath = "";
for (let i = 0; i < 5; i++) {
const checkPath = path.resolve(dir, ".env");
if (fs.existsSync(checkPath)) {
envPath = checkPath;
break;
}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
if (envPath) {
process.loadEnvFile(envPath);
}
} catch (e) {
// Ignore env loading errors
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Which Node.js version added process.loadEnvFile

💡 Result:

The process.loadEnvFile() method was added to Node.js in version 21.7.0 [1]. It was introduced alongside util.parseEnv() to allow for the programmatic loading and parsing of .env files into process.env [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## Files\n'
git ls-files 'web/package.json' 'package.json' 'web/next.config.ts' 'web/lib/supabase-client.ts' 'web/.nvmrc' '.nvmrc' 'web/.node-version' '.node-version' 'web/.tool-versions' '.tool-versions' | sed 's#^`#-` #'

printf '\n## web/package.json\n'
if [ -f web/package.json ]; then
  cat -n web/package.json | sed -n '1,220p'
fi

printf '\n## root package.json\n'
if [ -f package.json ]; then
  cat -n package.json | sed -n '1,220p'
fi

printf '\n## web/next.config.ts\n'
if [ -f web/next.config.ts ]; then
  cat -n web/next.config.ts | sed -n '1,220p'
fi

printf '\n## web/lib/supabase-client.ts\n'
if [ -f web/lib/supabase-client.ts ]; then
  cat -n web/lib/supabase-client.ts | sed -n '1,220p'
fi

printf '\n## Node version hints\n'
for f in web/.nvmrc .nvmrc web/.node-version .node-version web/.tool-versions .tool-versions; do
  if [ -f "$f" ]; then
    echo "--- $f"
    cat "$f"
  fi
done

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 3856


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for supported Node version declarations and next config environment loading usage.
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' \
  'process\.loadEnvFile|engines|node\s*[<>=]|NEXT_PUBLIC_SUPABASE_URL|NEXT_PUBLIC_SUPABASE_ANON_KEY|__dirname|loadEnvFile' \
  package.json web/package.json web/next.config.ts web/lib/supabase-client.ts .nvmrc .node-version .tool-versions 2>/dev/null || true

Repository: lgdlong/chatbot-rag-fptu-sdn

Length of output: 580


🌐 Web query:

Node.js release notes process.loadEnvFile 20.12.0 21.7.0

💡 Result:

The process.loadEnvFile function was introduced in Node.js version 20.12.0 and version 21.7.0 as a built-in programmatic API for loading.env files [1][2][3][4]. Key details regarding this feature include: Functionality: It loads environment variables from a specified.env file directly into process.env [5][6]. If no path is provided, it defaults to loading the.env file in the current working directory [2][4]. Usage: It is accessed via the node:process module [5][7]. Example usage includes: const { loadEnvFile } = require('node:process'); loadEnvFile; // Loads './.env' by default loadEnvFile('./path/to/.env'); // Loads a specific file Associated API: Along with process.loadEnvFile, Node.js introduced util.parseEnv(content), which allows for parsing a string of environment variable assignments into an object without automatically setting them on process.env [2][3][4]. Release Context: Node.js 20.12.0: This release (the LTS 'Iron' release) officially included the addition of both process.loadEnvFile and util.parseEnv [2][8]. Node.js 21.7.0: This release (the 'Current' release) also included these features and further improved.env file handling by introducing support for multi-line values [3][9]. Note that usage of NODE_OPTIONS within an.env file loaded via this method will not have any effect on the Node.js process [5][7].

Citations:


Surface .env load failures in web/next.config.ts
process.loadEnvFile needs Node 20.12.0+/21.7.0+, and this repo doesn’t declare a minimum Node version. If it’s unavailable or the .env parse fails, the empty catch leaves the Supabase env vars unset and the client throws later. Log or rethrow the load error instead of swallowing it.

🤖 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 `@web/next.config.ts` around lines 6 - 24, Update the top-level
environment-loading try/catch in next.config.ts to surface failures from
process.loadEnvFile, including unsupported Node versions and malformed .env
files. Replace the empty catch with a clear console error or rethrow that
includes the captured error, while preserving the existing upward .env search
behavior.

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.

5 participants