Skip to content

Conversation

@Peu77
Copy link
Member

@Peu77 Peu77 commented Jan 24, 2026

changed domain from coregame.de to coregame.sh
started refactoring everything to tanstack query
includes 390-migrate-authentication-to-the-backend

Summary by CodeRabbit

  • New Features

    • JWT cookie sessions, GitHub & 42 OAuth flows, SSO page, backend-driven 42 flow, automated repo provisioning, React Query provider, new loading UIs, standardized API error handling, client-side SSO redirect.
  • Infrastructure & Configuration

    • Primary domains, CORS, ingress, sitemaps and helm values switched to .sh; added API env vars and auth/runtime packages.
  • Removals

    • Frontend pre-commit hook, legacy header-based auth endpoints/guards, several frontend OAuth env docs and old join-notice component.
  • Documentation

    • Expanded API README with OAuth and environment setup guidance.

✏️ Tip: You can customize this high-level summary in your review settings.

Peu77 and others added 30 commits October 27, 2025 23:11
… details and update SSO page for seamless sign-in experience
…tion-to-the-backend

# Conflicts:
#	frontend/app/events/page.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Peu77 Peu77 requested a review from PaulicStudios January 24, 2026 12:16
@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

📝 Walkthrough

Walkthrough

Adds cookie-based JWT auth, GitHub and 42 OAuth backend flows; replaces header-based UserGuard with JwtAuthGuard; migrates domains from .de to .sh; introduces React Query hydration/mutations frontend-side; adds repo auto-creation with advisory locks and related DB migrations; removes client OAuth secrets/docs and a local pre-commit hook.

Changes

Cohort / File(s) Summary
Domain migration
README.md, .github/workflows/api-deploy.yml, .github/workflows/frontend-deploy.yml, frontend/helm/..., api/helm/..., k8s-service/kubernetes/wiki-redirect.yaml, s3/*/cors.xml, frontend/app/{layout,page,robots,sitemap,error}.tsx, frontend/layouts/footer.tsx, frontend/components/system/NotFoundClient.tsx
Replaced .de hostnames/URLs/badges/status links with .sh equivalents across CI, Helm, k8s, S3 CORS and frontend base URLs.
API: auth core & strategies
api/package.json, api/.env.example, api/README.md, api/src/auth/{auth.controller.ts,auth.module.ts,auth.service.ts,jwt.strategy.ts,jwt-auth.guard.ts,github.strategy.ts,fortytwo.strategy.ts}, api/src/types/passport-oauth2.d.ts
Added JWT/JwtModule and cookie-based JwtStrategy/guard, AuthController endpoints (/github/callback, /42/getUrl, /42/callback, /me), GitHub and 42 OAuth strategies, new env vars, docs and jwt-related deps.
API: guards & user/social changes
api/src/guards/{UserGuard.ts,GuardConstants.ts,TeamGuard.ts}, api/src/{event,match,team,user,social-account}.controller.ts, api/src/user/social-account.service.ts
Removed header-based UserGuard and exported USER_ID_KEY; UserId decorator now reads request.user; usages replaced with JwtAuthGuard; removed linkSocialAccount endpoint and added upsertSocialAccountForUser.
API: runtime, locks & migrations
api/src/main.ts, api/db/migrations/*, api/src/constants.ts, api/src/team/team.service.ts, api/src/team/entities/team.entity.ts, api/src/github-api/github-api.service.ts, api/src/match/match.service.ts, api/src/event/*
Added cookie-parser and explicit CORS setup; added DB migrations (drop event state enum, add startedRepoCreationAt); introduced LockKeys enum; removed EventState usage; added cron-based repo auto-creation with advisory locks and transactional repo provisioning; updated services to use locks/constants.
Frontend: auth flow & axios
frontend/app/utils/authOptions.ts, frontend/app/auth/sso/page.tsx, frontend/app/actions/axios.ts, frontend/app/actions/social-accounts.ts, frontend/components/github.tsx, frontend/hooks/use42Linking.ts, frontend/.env.example, frontend/README.md
Switched to backend-mediated auth (Credentials/SSO), removed client OAuth secrets/docs, axios now uses cookie-based auth with BACKEND_URL / NEXT_PUBLIC_BACKEND_PUBLIC_URL branching; added endpoint to fetch 42 auth URL and SSO client page.
Frontend: React Query migration
frontend/app/providers.tsx, frontend/app/events/[id]/my-team/{page.tsx,queries.ts,teamView.tsx}, frontend/app/events/[id]/my-team/components/{TeamCreationForm.tsx,TeamInfoDisplay.tsx,TeamInvitesDisplay.tsx}, frontend/app/events/[id]/queue/{page.tsx,queries.ts,queueState.tsx}, plus related event pages/components, frontend/package.json
Added @tanstack/react-query, a QueryClient provider, server-side prefetch/dehydrate and client hydration; replaced polling/direct fetches with useQuery/useMutation and cache invalidation; introduced query helper modules and adjusted components to use queries/mutations.
Frontend: UI/types & small behavior
frontend/app/{events,loading,wiki/loading}.tsx, frontend/components/team/*, frontend/app/profile/ProfileClient.tsx, frontend/types/next-auth.d.ts, various small components
Added loading components, switched session image field to profilePicture (type updated), tightened props to Readonly, added Spinner usage and minor UI/behavior tweaks.
API & services: repo multi-user changes
github-service/src/*, api/src/github-api/github-api.service.ts, github-service/src/repo.utils.ts
Changed createTeamRepository payloads to accept githubUsers[] (username + token) and updated downstream handling in github-service; updated repo folder constant.
Frontend: removed/relocated features & docs
frontend/app/actions/event-model.ts, frontend/app/actions/event.ts, frontend/app/actions/team.ts, frontend/components/event-join-notice.tsx, .githooks/pre-commit, frontend/README.md, frontend/.env.example
Removed EventState enum and related helpers, removed frontend createTeam action and event-join-notice component, removed local pre-commit hook and client OAuth docs/vars.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Client Browser
    participant Frontend as Frontend App
    participant Backend as Backend API
    participant OAuth as OAuth Provider
    rect rgba(100,150,200,0.5)
    note over Browser,OAuth: GitHub OAuth (backend-mediated)
    Browser->>Frontend: Click "Sign in with GitHub"
    Frontend->>Backend: Navigate /auth/github (redirect)
    Backend->>OAuth: Redirect user to OAuth authorize endpoint
    OAuth-->>Backend: Return code to callback
    Backend->>Backend: Exchange code, create/update user, sign JWT
    Backend-->>Browser: Set httpOnly cookie and redirect to app
    Browser->>Frontend: Subsequent requests include cookie JWT
    end
Loading
sequenceDiagram
    participant Browser as Client Browser
    participant Frontend as Frontend App
    participant Backend as Backend API
    participant OAuth42 as Forty-Two Provider
    rect rgba(150,200,100,0.5)
    note over Browser,OAuth42: 42 School linking flow
    Browser->>Frontend: Request 42 auth URL
    Frontend->>Backend: GET /auth/42/getUrl
    Backend->>Backend: Encrypt user id, return auth URL
    Backend-->>Browser: OAuth authorization URL
    Browser->>OAuth42: Redirect to 42 authorization
    OAuth42-->>Browser: Redirect to /auth/42/callback (with code + state)
    Browser->>Backend: GET /auth/42/callback
    Backend->>Backend: Decrypt state, upsert social account
    Backend-->>Browser: Redirect to success URL
    end
Loading
sequenceDiagram
    participant Browser as Client Browser
    participant Frontend as Frontend App (React Query)
    participant Backend as Backend API
    rect rgba(200,150,100,0.5)
    note over Browser,Backend: Protected API request via cookie JWT
    Browser->>Frontend: Render component needing team data
    Frontend->>Frontend: Check React Query cache
    Frontend->>Backend: GET /team/event/{eventId}/my (cookies sent)
    Backend->>Backend: Validate JWT from cookie
    Backend-->>Frontend: Return team data
    Frontend->>Frontend: Update cache and render UI
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped from .de to shiny .sh,

Cookies tuck tokens snug and hush.
GitHub and Forty‑Two danced through the gate,
Queries cache, repos spin up late.
A rabbit cheers — new flows stitched, first-rate!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'tanstack query migration' directly describes the main technical change: refactoring the codebase to use TanStack Query. This is the primary focus of the changeset and is clearly summarized.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (10)
frontend/app/events/[id]/dashboard/dashboard.tsx (4)

246-246: Remove empty .finally() block.

This empty callback serves no purpose and should be removed.

Proposed fix
                        setStartingGroupPhase(false);
                      })
-                     .finally(() => {});
+                     ;

270-270: Remove empty .finally() block.

Same issue as above—dead code that should be removed.

Proposed fix
                        setStartingTournament(false);
                      })
-                     .finally(() => {});
+                     ;

289-300: Add error handling for consistency.

The setEventTeamsLockDate promise lacks a .catch() handler, unlike other action handlers in this file. If the request fails, the user won't receive any feedback.

Proposed fix
                  onClick={() =>
                    setEventTeamsLockDate(
                      eventId,
                      new Date(teamAutoLockTime).getTime(),
                    ).then(() => {
                      // eslint-disable-next-line no-alert
                      alert("set team auto lock date");
-                   })}
+                   }).catch(() => {
+                     // eslint-disable-next-line no-alert
+                     alert("error occurred");
+                   })}

301-312: Add error handling for the Reset action.

Same concern—missing .catch() handler leaves users uninformed on failure.

Proposed fix
                  onClick={() => {
                    setEventTeamsLockDate(eventId, null).then(() => {
                      // eslint-disable-next-line no-alert
                      alert("reset team auto lock date");
                      setTeamAutoLockTime("");
-                   });
+                   }).catch(() => {
+                     // eslint-disable-next-line no-alert
+                     alert("error occurred");
+                   });
                  }}
frontend/components/team/TeamInviteModal.tsx (2)

32-36: Unused teamId prop.

The teamId prop is defined in TeamInviteModalProps (line 28) but is not destructured or used anywhere in the component. Either remove it from the interface or use it in sendTeamInvite if the API requires it.

Option 1: Remove unused prop from interface
 interface TeamInviteModalProps {
   isOpen: boolean;
   onClose: () => void;
-  teamId: string;
   eventId: string;
 }

105-111: Nested DialogContent components.

There's a DialogContent at line 105 and another nested inside at line 111. Radix/shadcn Dialog components don't support nested DialogContent. The inner one should likely be a <div> or removed entirely.

Suggested fix
       <DialogContent>
         <DialogHeader>
           <DialogTitle className="text-xl font-semibold">
             Invite Team Members
           </DialogTitle>
         </DialogHeader>
-        <DialogContent>
+        <div className="py-4">
           <div className="grid w-full max-w-sm items-center gap-3">
             ...
           </div>
           <div className="max-h-[300px] overflow-y-auto">
             ...
           </div>
-        </DialogContent>
+        </div>
         <DialogFooter>
frontend/components/team/TeamInfoSection.tsx (1)

64-75: Surface leave failures while the dialog stays open.
On failure you return early, but the error message is rendered outside the modal, so it’s hidden while the dialog is open. Consider closing the dialog on failure or rendering the error inside the dialog.

✅ Minimal fix (close dialog on failure)
 const success = await onLeaveTeam();
 if (!success) {
   setLeaveError(
     "Failed to leave team. Try refreshing the page or trying again later.",
   );
+  setIsLeaveDialogOpen(false);
   return;
 }
frontend/helm/README.md (1)

87-92: Update TLS secret name to match .sh domain (doc mismatch).

Line 92 still references www-coregame-de-tls, which no longer matches the .sh domain shown in Lines 87-88. This can mislead operators copying the example.

📝 Suggested doc fix
-    secretName: www-coregame-de-tls
+    secretName: www-coregame-sh-tls
frontend/.env.example (1)

20-35: Fix dotenv‑linter warnings (quote + key order).

Line 20 has quotes and Line 35 wants ordering; minor but will keep lint clean.

✅ Suggested adjustment
-NEXT_PUBLIC_VISUALIZER_URL="https://dev.visualizer.coregame.sh"
+NEXT_PUBLIC_VISUALIZER_URL=https://dev.visualizer.coregame.sh
@@
-PLAUSIBLE_DOMAIN=local.coregame.sh
-PLAUSIBLE_CUSTOM_DOMAIN=https://plausible.coregame.sh
+PLAUSIBLE_CUSTOM_DOMAIN=https://plausible.coregame.sh
+PLAUSIBLE_DOMAIN=local.coregame.sh
frontend/layouts/basic-navbar.tsx (1)

191-197: Add a fallback for profilePicture to prevent next/image error.

If the session API call fails (line 71 in authOptions.ts), profilePicture may be undefined while session?.user?.id is still set, causing the Image component to throw an error. Either wrap the Image in a conditional render or provide a fallback URL:

Suggested fix (requires creating `/public/placeholder-avatar.png` or use existing fallback)
-                          src={session?.user?.profilePicture}
+                          src={session?.user?.profilePicture || "/placeholder-avatar.png"}

Alternatively, conditionally render the Image only when profilePicture exists.

🤖 Fix all issues with AI agents
In `@api/.env.example`:
- Line 18: Rename the invalid environment variable that starts with a digit:
change 42_OAUTH_SUCCESS_REDIRECT_URL to FORTYTWO_OAUTH_SUCCESS_REDIRECT_URL in
the .env.example and update the reference inside the Auth controller where
configService.getOrThrow(...) is called (search for the string
"42_OAUTH_SUCCESS_REDIRECT_URL" in auth.controller.ts) so the same key is used;
also update any other occurrences (tests, docs, or other config lookups) to the
new FORTYTWO_OAUTH_SUCCESS_REDIRECT_URL name to keep usage consistent.

In `@api/helm/values-dev.yaml`:
- Around line 43-46: The TLS secretName currently references the old domain
(secretName: dev-api-coregame-de-tls) while hosts lists dev.api.coregame.sh;
update the secretName to match the new domain (e.g., change secretName to a name
reflecting .sh) and ensure cert-manager is instructed to issue a certificate for
dev.api.coregame.sh, or alternatively confirm the existing secret contains a
certificate valid for dev.api.coregame.sh before leaving secretName unchanged;
adjust the tls block's secretName and/or recreate the TLS secret so the
certificate SANs match the hosts entry.

In `@api/src/user/social-account.service.ts`:
- Around line 52-75: The upsertSocialAccountForUser implementation is
race-prone; replace the findOne-then-save flow with an atomic upsert using
this.socialAccountRepository.upsert(...) keyed on userId and platform to avoid
unique constraint errors; after calling socialAccountRepository.upsert({ userId,
platform, username, platformUserId }, { conflictPaths: ['userId','platform'] })
fetch and return the persisted entity via this.socialAccountRepository.findOne({
where: { userId: params.userId, platform: params.platform } }) so the method
still returns a SocialAccountEntity.

In `@frontend/app/auth/sso/page.tsx`:
- Around line 9-23: The async sign-in flow currently ignores the response from
signIn and treats failures the same as successes; update the useEffect handler
(the async IIFE that calls signIn) to await and inspect the returned response
object from signIn("backend", { redirect: false }) instead of assuming it
throws: if res.error or !res.ok then, when not cancelled, call
router.replace("/auth/error"); else if res.url is present then navigate to the
provider redirect (e.g., set window.location.href = res.url) instead of
router.replace("/"), and only call router.replace("/") and router.refresh() when
res.ok is true and no external res.url exists; keep the try/catch to log real
exceptions.

In `@frontend/app/events/`[id]/my-team/components/TeamInfoDisplay.tsx:
- Around line 39-49: The query key in TeamInfoDisplay.tsx is inconsistent with
SSR prefetching; replace the inline array key ["team", team?.id, "members"] used
in the useQuery call with the shared helper teamMembersQueryKey(team?.id) from
the queries module so the client query matches server-side keys (keep other
options like queryFn, enabled, and initialData unchanged).
- Around line 27-37: Add an onSuccess handler to the leaveTeamMutation so that
after a successful API call you invalidate the cached queries for the current
event's team and pending invites (e.g., invalidateQueries(["event", eventId,
"my-team"]) and invalidateQueries(["event", eventId, "pending-invites"]) via the
react-query QueryClient) to ensure the UI updates immediately; also remove the
unused isRepoPending state and stop passing it into TeamInfoSection (delete its
declaration and the prop usage). Use the existing eventId and leaveTeamMutation
identifiers to locate where to attach the onSuccess and call
queryClient.invalidateQueries.

In `@frontend/app/events/`[id]/my-team/queries.ts:
- Around line 21-26: The teamMembersQueryFn currently accepts teamId?: string
and will call axiosInstance.get(`/team/${teamId}/members`) even when teamId is
undefined; update teamMembersQueryFn to guard against undefined teamId by either
(A) changing the parameter type to string and relying on the caller to only call
it when teamId exists (and use TanStack Query's enabled option in the consumer),
or (B) keeping the signature but early-return or throw a clear error when teamId
is undefined before calling axiosInstance.get; reference teamMembersQueryFn and
the axiosInstance.get call when making the change so the request to
`/team/undefined/members` is prevented.

In `@frontend/app/events/`[id]/queue/queueState.tsx:
- Around line 33-61: The refetchInterval on the queueState useQuery is set to
600 (milliseconds) which is too aggressive; change the refetchInterval passed to
the useQuery for queueState (the call using queueStateQueryKey/eventId and
queryFn queueStateQueryFn) to a larger value (e.g. 3000 ms) or replace the
literal with a named constant (e.g. QUEUE_POLL_INTERVAL) and document it so the
intent is clear and easily configurable; update any related comments or
constants accordingly.
- Around line 63-79: The redirect compares the live queueState to the static
props.queueState, so it never detects client-side transitions; replace the
props-based previous state with a ref: create a prevMatchStateRef via useRef,
initialize it to props.queueState.match?.state, then in the useEffect that
watches queueState (and router/eventId) compare prevMatchStateRef.current to
queueState.match?.state (checking for transition from MatchState.IN_PROGRESS to
a new match), call router.push when appropriate, and finally update
prevMatchStateRef.current = queueState.match?.state; ensure you remove
props.queueState.match?.state from the effect deps and reference the symbols
queueState, props.queueState, prevMatchStateRef, useEffect,
MatchState.IN_PROGRESS, and router.push to locate the code.

In `@frontend/app/events/page.tsx`:
- Around line 52-54: The Link component usage in events/page.tsx incorrectly
passes a non-existent color prop to next/link; remove the color="primary" prop
from the <Link> element and instead add a Tailwind class via className (for
example className="text-primary" or whatever design token you use) to style the
link; update the <Link> with className and ensure any nested children receive
the styling as needed (reference the Link element in this file).

In `@frontend/app/providers.tsx`:
- Around line 6-14: The module-level QueryClient instance (const queryClient) in
Providers leads to cross-request cache leakage; remove that top-level
instantiation and implement a getQueryClient() helper that returns a new
QueryClient when isServer() is true (per-request SSR) and a stable singleton in
the browser, then update the Providers component to call getQueryClient() and
pass its result to <QueryClientProvider client={...}> while preserving
ReactQueryDevtools and children; ensure the unique symbols referenced are
QueryClient, Providers, QueryClientProvider, getQueryClient, and isServer.

In `@frontend/app/utils/authOptions.ts`:
- Around line 58-73: On failure of the axiosInstance.get('/auth/me') call, clear
all session user fields instead of only session.user.id to avoid stale
JWT-derived data; in the catch block set session.user.id, session.user.name,
session.user.email, and session.user.profilePicture to empty values (or add/set
an explicit session.user.isAuthenticated = false and handle that at the call
site) so useSession() no longer reports an authenticated user with outdated
information.

In `@frontend/components/github.tsx`:
- Around line 7-10: The redirect builds a broken URL when
process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL is missing; update the logic around
the code that reads NEXT_PUBLIC_BACKEND_PUBLIC_URL and sets window.location.href
(the block using base = process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL and then
window.location.href = `${base?.replace(/\/$/, "")}/auth/github/callback`) to
validate base first, and if it's falsy, log a clear error (or surface a UI
error) and do not perform the redirect; only call replace(...) and assign
window.location.href when base is a non-empty string.
🟡 Minor comments (8)
frontend/components/team/TeamInfoSection.tsx-219-226 (1)

219-226: Keep a readable label when showing the spinner.
When loading, the button text disappears, which reduces clarity and screen-reader context.

♿ Accessible loading label
-  {isLeaving ? <Spinner /> : "Leave Team"}
+  {isLeaving ? (
+    <>
+      <Spinner />
+      <span className="sr-only">Leaving team</span>
+    </>
+  ) : (
+    "Leave Team"
+  )}
frontend/app/events/[id]/my-team/components/TeamCreationForm.tsx-39-43 (1)

39-43: Analytics event fires before mutation succeeds.

The plausible("create_team") call in onMutate triggers even when the mutation subsequently fails. For accurate analytics, track successful team creations in onSuccess instead.

Proposed fix
     onMutate: () => {
-      plausible("create_team");
       setErrorMessage(null);
       setValidationError(null);
     },
     onSuccess: async () => {
+      plausible("create_team");
       await Promise.all([
api/package.json-50-51 (1)

50-51: Add @types/passport-jwt to devDependencies.

The codebase uses passport-jwt (imported in api/src/auth/jwt.strategy.ts), but @types/passport-jwt is missing from devDependencies. While @types/passport-local is already included, the corresponding type definitions for the JWT strategy are absent. Since passport-jwt does not ship with built-in TypeScript types, the types package should be added for proper TypeScript support.

api/README.md-109-126 (1)

109-126: Duplicate section heading creates documentation confusion.

There is already an "## Environment Variables" section at line 64. Having two sections with the same heading makes navigation difficult and breaks document structure.

Proposed fix

Merge this OAuth setup content into the existing "Environment Variables" section (around lines 64-85), perhaps as new subsections:

-## Environment Variables
-
-### GitHub OAuth (Required)
+### GitHub OAuth (Required for Authentication)

Move lines 112-126 to follow line 85, removing the duplicate ## Environment Variables heading.

api/helm/values-prod.yaml-48-51 (1)

48-51: TLS secret name still references old domain.

The TLS hosts have been updated to api.coregame.sh, but secretName still references api-coregame-de-tls. This inconsistency should be corrected:

  • The secret name should reflect the domain it serves for clarity and consistency
  • During a domain migration, this mismatch may cause confusion in operations and troubleshooting
  • cert-manager will manage the secret regardless, but best practice is to keep naming conventions aligned
🔧 Proposed fix
   tls:
     - hosts:
         - api.coregame.sh
-      secretName: api-coregame-de-tls
+      secretName: api-coregame-sh-tls
frontend/app/events/[id]/my-team/components/TeamInvitesDisplay.tsx-108-124 (1)

108-124: Button disabled states don't prevent double-clicks during own action.

The Accept button is disabled only when isDeclining (line 111), not when isAccepting. Similarly, the Decline button is disabled only when isAccepting (line 120), not when isDeclining. This allows users to click the same button multiple times during the action.

🐛 Proposed fix
                   <Button
                     size="sm"
                     // TODO: isLoading={actionStates[invite.id]?.isAccepting}
-                    disabled={actionStates[invite.id]?.isDeclining}
+                    disabled={actionStates[invite.id]?.isAccepting || actionStates[invite.id]?.isDeclining}
                     onClick={() => handleAcceptInvite(invite.id)}
                   >
                     Accept
                   </Button>
                   <Button
                     size="sm"
                     variant="secondary"
                     // TODO: isLoading={actionStates[invite.id]?.isDeclining}
-                    disabled={actionStates[invite.id]?.isAccepting}
+                    disabled={actionStates[invite.id]?.isAccepting || actionStates[invite.id]?.isDeclining}
                     onClick={() => handleDeclineInvite(invite.id)}
                   >
                     Decline
                   </Button>
frontend/app/events/[id]/queue/queries.ts-20-30 (1)

20-30: Inconsistent endpoint path format within the same file.

The /match/queue/${eventId}/ endpoint uses a leading slash while the other queue endpoints in this file (team/event/${eventId}/queue/state and team/event/${eventId}/queue/join) use relative paths. Standardize on relative paths for consistency across the queue operations:

🔧 Proposed fix
-  const response = await axiosInstance.get<Match[]>(
-    `/match/queue/${eventId}/`,
-  );
+  const response = await axiosInstance.get<Match[]>(
+    `match/queue/${eventId}/`,
+  );

Note: This same inconsistency exists in frontend/app/actions/team.ts (lines 42 & 46) where /match/queue/ is used with a leading slash alongside relative team/event/ paths in the same file.

frontend/app/events/[id]/queue/page.tsx-51-66 (1)

51-66: Avoid mutating cached array when sorting.
match.teams.sort(...) mutates the cached array in-place. While this works in this SSR context, it violates React Query's immutability principles and can cause subtle bugs if the code is refactored. Clone before sorting to keep cached data immutable.

🔧 Proposed fix
-  const sortedQueueMatches = queueMatches.map(match => ({
-    ...match,
-    teams: match.teams.sort((a, _b) => (a.id === myTeam.id ? -1 : 1)),
-  }));
+  const sortedQueueMatches = queueMatches.map(match => ({
+    ...match,
+    teams: [...match.teams].sort((a, _b) => (a.id === myTeam.id ? -1 : 1)),
+  }));
🧹 Nitpick comments (29)
frontend/app/error.tsx (1)

64-68: Minor indentation inconsistency in string concatenation.

The + operators on lines 64 and 68 have extra indentation compared to the preceding lines (54-63), which slightly breaks the visual alignment of the multiline string.

✨ Suggested fix for consistent indentation
                 + `- OS: \n`
                 + `- Device: \n`
-                  + `- Time (UTC): ${new Date()
-                    .toISOString()
-                    .replace("T", " ")
-                    .replace("Z", " UTC")}\n\n`
-                    + `Additional context:\n`,
+                + `- Time (UTC): ${new Date()
+                  .toISOString()
+                  .replace("T", " ")
+                  .replace("Z", " UTC")}\n\n`
+                + `Additional context:\n`,
frontend/app/sitemap.ts (1)

4-4: Consider stripping trailing slashes from the base URL.

The domain migration looks correct. However, if NEXTAUTH_URL is configured with a trailing slash, the concatenated URLs would produce malformed paths like https://coregame.sh//wiki.

Suggested defensive fix
-  const baseUrl = process.env.NEXTAUTH_URL || "https://coregame.sh";
+  const baseUrl = (process.env.NEXTAUTH_URL || "https://coregame.sh").replace(/\/+$/, "");
frontend/app/events/loading.tsx (1)

1-6: Optional: extract a shared centered loading spinner component.

These loading files duplicate the same Spinner markup. A tiny shared component would reduce repetition and keep styling consistent.

♻️ Proposed change (this file)
-import { Spinner } from "@/components/ui/spinner";
+import { CenteredSpinner } from "@/components/ui/centered-spinner";

 export default function Loading() {
   return (
-    <Spinner className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-24" />
+    <CenteredSpinner />
   );
 }
➕ Suggested new component (new file)
// frontend/components/ui/centered-spinner.tsx
import { Spinner } from "@/components/ui/spinner";

export function CenteredSpinner() {
  return (
    <Spinner className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 size-24" />
  );
}
frontend/components/team/TeamCreationSection.tsx (1)

36-41: LGTM — Loading state implementation is correct.

The button correctly disables during loading and provides visual feedback with the Spinner. This properly prevents double-submission.

Optional accessibility improvement: When the button shows only the Spinner, screen readers lose context about the button's purpose. Consider adding visually hidden text:

{isLoading ? (
  <>
    <Spinner />
    <span className="sr-only">Creating team...</span>
  </>
) : (
  "Create Team"
)}
frontend/app/events/[id]/dashboard/dashboard.tsx (1)

35-80: Consider migrating to TanStack Query.

Given the PR objective is TanStack Query migration, this component still uses useState + useEffect for data fetching. Consider refactoring to use useQuery for getEventById, getTeamsCountForEvent, getParticipantsCountForEvent, and isEventAdmin, which would provide automatic caching, refetching, and loading/error states.

frontend/components/team/TeamInviteModal.tsx (2)

171-178: Address the TODO: Button loading state.

The isInviting[user.id] state is already tracked but not used to show loading feedback on the button. Consider adding a loading spinner or disabling the button during invite.

Suggested implementation
                           <Button
                             size="sm"
-                            disabled={user.isInvited}
-                            // TODO: isLoading={isInviting[user.id]}
+                            disabled={user.isInvited || isInviting[user.id]}
                             onClick={() => handleInviteUser(user.id)}
                           >
-                            {user.isInvited ? "Invited" : "Invite"}
+                            {isInviting[user.id] 
+                              ? "Inviting..." 
+                              : user.isInvited 
+                                ? "Invited" 
+                                : "Invite"}
                           </Button>

Do you want me to open a new issue to track this task?


46-57: Consider migrating to TanStack Query.

Given the PR's TanStack Query migration scope, this search logic would benefit from useQuery with the debounced query as a key. This would handle caching, deduplication, and stale data automatically, while eliminating potential race conditions from rapid typing.

const { data: searchResults = [], isLoading: isSearching } = useQuery({
  queryKey: ['userSearch', eventId, debouncedQuery],
  queryFn: () => searchUsersForInvite(eventId, debouncedQuery),
  enabled: debouncedQuery.length >= 2,
});
frontend/components/team/TeamInfoSection.tsx (1)

92-105: Consider an explicit empty state instead of a perpetual skeleton.
If a repo is genuinely unset, the skeleton looks like a never-ending load. A simple placeholder text is clearer, unless you still have a loading state to gate it.

💡 Possible empty-state tweak
-                : (
-                    <Skeleton className="h-5 w-75 rounded-md m-2" />
-                  )}
+                : (
+                    <span className="text-muted-foreground">No repository configured</span>
+                  )}
frontend/components/social-accounts-display.tsx (1)

72-76: Prefer a non-blocking dialog/toast instead of disabling lint for confirm/alert.
Using native dialogs blocks the UI and forces lint suppression; consider a modal/toast-based confirmation + error feedback to avoid no-alert disables.

Also applies to: 87-91

frontend/app/events/[id]/my-team/components/TeamCreationForm.tsx (1)

66-68: Unhandled promise rejection when mutation fails.

Using mutateAsync() without a try/catch means errors thrown by the mutation will result in unhandled promise rejections. While onError handles UI state, the thrown error propagates to the caller.

Consider using mutate() instead (which doesn't throw) or wrap in try/catch if you need the async behavior.

Option 1: Use mutate() instead
-  async function handleCreateTeam() {
-    await createTeamMutation.mutateAsync();
+  function handleCreateTeam() {
+    createTeamMutation.mutate();
   }
Option 2: Keep mutateAsync with try/catch
   async function handleCreateTeam() {
-    await createTeamMutation.mutateAsync();
+    try {
+      await createTeamMutation.mutateAsync();
+    } catch {
+      // Error already handled in onError callback
+    }
   }
frontend/helm/values-prod.yaml (1)

49-52: TLS secret name still references old domain.

The host is now coregame.sh but secretName remains coregame-de-tls. While this may still work if the existing certificate covers both domains, consider updating the secret name to coregame-sh-tls for consistency and to avoid confusion.

The same issue exists on line 64 with www-coregame-de-tls.

Proposed fix
   tls:
     - hosts:
         - coregame.sh
-      secretName: coregame-de-tls
+      secretName: coregame-sh-tls
   tls:
     enabled: true
-    secretName: www-coregame-de-tls
+    secretName: www-coregame-sh-tls

Verify whether the existing TLS secrets contain certificates valid for the new .sh domain, or if new certificates will be provisioned by cert-manager under these secret names.

frontend/app/actions/axios.ts (2)

16-28: Avoid dropping non‑token cookies in server‑side requests.

Line 18–22 overwrites the Cookie header with only token=…, which discards other cookies (e.g., refresh, CSRF, feature flags). Consider forwarding all cookies from next/headers to keep future auth changes safe.

♻️ Suggested adjustment
-      const cookieData = await require("next/headers").cookies();
-      const token = cookieData.get("token");
-      if (token)
-        config.headers.Cookie = `token=${token.value}`;
+      const cookieData = await require("next/headers").cookies();
+      const cookieHeader = cookieData
+        .getAll()
+        .map(({ name, value }) => `${name}=${value}`)
+        .join("; ");
+      if (cookieHeader)
+        config.headers.Cookie = cookieHeader;

36-46: Narrow error handling to AxiosError for safer messaging.

error: any can mask non‑Axios errors and lose useful messages. Prefer axios.isAxiosError and fall back to Error.message when not Axios.

♻️ Suggested adjustment
-import axios from "axios";
+import axios from "axios";
@@
-  catch (error: any) {
-    return {
-      error: error.response?.data?.message || "An unexpected error occurred",
-    };
-  }
+  catch (error: unknown) {
+    if (axios.isAxiosError(error)) {
+      return {
+        error:
+          error.response?.data?.message
+          || error.message
+          || "An unexpected error occurred",
+      };
+    }
+    return {
+      error:
+        error instanceof Error
+          ? error.message
+          : "An unexpected error occurred",
+    };
+  }
frontend/hooks/use42Linking.ts (1)

42-69: Clear the pending timeout to avoid setState after unmount.

Line 48 schedules async work via setTimeout; if the component unmounts before it fires, state updates can trigger warnings. Track the timeout id and clear it on unmount (or before re‑initiating).

♻️ Suggested adjustment
-  const processedRef = useRef<string | null>(null);
+  const processedRef = useRef<string | null>(null);
+  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@
-    setTimeout(async () => {
+    if (timeoutRef.current)
+      clearTimeout(timeoutRef.current);
+    timeoutRef.current = setTimeout(async () => {
@@
-    }, OAUTH_CONFIG.LOADING_DELAY);
+    }, OAUTH_CONFIG.LOADING_DELAY);
   }, []);
+
+  useEffect(() => {
+    return () => {
+      if (timeoutRef.current)
+        clearTimeout(timeoutRef.current);
+    };
+  }, []);
api/.env.example (1)

23-23: Add trailing newline at end of file.

Per static analysis and POSIX convention, files should end with a newline character.

Proposed fix
 FORTYTWO_CLIENT_SECRET=
+
api/src/user/social-account.controller.ts (1)

21-25: Remove unused LinkSocialAccountDto class.

This DTO class is no longer used since the linkSocialAccount endpoint was removed as part of the authentication refactor. Dead code should be cleaned up to maintain code hygiene.

🧹 Proposed fix to remove unused code
-class LinkSocialAccountDto {
-  platform: SocialPlatform;
-  username: string;
-  platformUserId: string;
-}
-
 `@UseGuards`(JwtAuthGuard)
api/src/auth/fortytwo.strategy.ts (2)

43-46: Replace console.error with NestJS Logger for consistency.

Using console.error directly bypasses NestJS's logging infrastructure. Other parts of the codebase (e.g., MatchController) use Logger from @nestjs/common for structured logging.

♻️ Proposed fix using NestJS Logger
+import { Injectable, Logger } from "@nestjs/common";
-import { Injectable } from "@nestjs/common";
 ...

 `@Injectable`()
 export class FortyTwoOAuthStrategy extends PassportStrategy(Strategy, "42") {
+  private readonly logger = new Logger(FortyTwoOAuthStrategy.name);
+
   constructor(
 ...
       if (!res.ok) {
-        console.error("42 API error:", res.status, await res.text());
+        this.logger.error(`42 API error: ${res.status} - ${await res.text()}`);
         return done(new Error(`42 API error: ${res.status}`));
       }

18-22: Remove unused injected services to reduce unnecessary coupling.

UserService and SocialAccountService are injected in the constructor but never used within this strategy. The validate method only returns the 42 account data via the done callback, and user creation/linking happens elsewhere. These unused dependencies should be removed.

♻️ Proposed fix
 `@Injectable`()
 export class FortyTwoOAuthStrategy extends PassportStrategy(Strategy, "42") {
   constructor(
     config: ConfigService,
-    private readonly users: UserService,
-    private readonly socialAccounts: SocialAccountService,
   ) {

Remove the corresponding import statements for UserService and SocialAccountService as well.

frontend/app/events/[id]/my-team/components/TeamInvitesDisplay.tsx (2)

110-111: TODO comments for loading states should be addressed.

The isLoading prop is commented out for both buttons. Consider using the Button component's loading indicator (if supported) or wrapping with a spinner to provide visual feedback during async operations.

Would you like me to open an issue to track implementing the loading states for these buttons?

Also applies to: 119-120


54-57: Inconsistent state management between accept and decline handlers.

handleAcceptInvite uses router.refresh() which triggers a full page data refetch, while handleDeclineInvite updates local state with setInvites. This inconsistency could cause UX issues if the server state diverges from local state. Consider using the same pattern for both, preferably leveraging React Query's invalidateQueries for consistency with the TanStack Query migration.

Also applies to: 78-78

api/src/auth/auth.module.ts (1)

17-26: Consider security implications of 30-day JWT expiry.

A 30-day token lifetime is quite long. If a token is compromised (e.g., cookie theft via XSS), the attacker has an extended window. Common practice is shorter-lived access tokens (e.g., 15 minutes to a few hours) with refresh tokens for session extension. Since this is stored in an httpOnly cookie, the risk is reduced but not eliminated.

If 30 days is intentional for UX reasons, consider adding token revocation capability (e.g., storing token IDs in a blocklist on logout or password change).

api/src/auth/jwt.strategy.ts (1)

37-44: Error handling swallows all errors from getUserById.

Using .catch(() => null) treats database/network errors the same as "user not found". A transient DB issue would result in an UnauthorizedException rather than a 500 error, potentially confusing debugging. Consider logging the error or distinguishing between "not found" and other failures.

♻️ Optional: Distinguish error types
   async validate(payload: JwtPayload) {
     const userId = payload.sub;
     if (!userId) throw new UnauthorizedException();
     // Load current user; if missing, reject token
-    const user = await this.users.getUserById(userId).catch(() => null);
+    const user = await this.users.getUserById(userId).catch((err) => {
+      // Log unexpected errors for debugging; still return null to trigger 401
+      console.error(`Failed to load user ${userId}:`, err);
+      return null;
+    });
     if (!user) throw new UnauthorizedException();
     return user; // attaches to request.user
   }
api/src/auth/github.strategy.ts (1)

40-50: Consider combining update and refetch into a single operation.

After updateUser, the code immediately fetches the same user with getUserById. If updateUser could return the updated entity, this would eliminate an extra database round-trip.

frontend/app/events/[id]/my-team/components/TeamInfoDisplay.tsx (2)

51-65: Missing cache invalidation after successful leave.

The mutation lacks an onSuccess handler to invalidate the team query cache. After leaving, the UI relies solely on the 3-second refetch interval to update, causing a brief period where stale data is shown.

Proposed fix using queryClient invalidation
+import { useQueryClient } from "@tanstack/react-query";
 ...
+const queryClient = useQueryClient();
+
 const leaveTeamMutation = useMutation({
   mutationFn: async () => {
     const result = await leaveTeam(eventId);
     if (isActionError(result)) {
       throw new Error(result.error);
     }
   },
   onMutate: () => {
     plausible("leave_team");
     setErrorMessage(null);
   },
+  onSuccess: () => {
+    queryClient.invalidateQueries({ queryKey: ["event", eventId, "my-team"] });
+  },
   onError: (error: Error) => {
     setErrorMessage(error.message);
   },
 });

24-24: Dead state: isRepoPending is always false.

This state is initialized to false and never updated. If timer-based polling was intentionally removed, consider removing this state entirely or implementing the intended functionality.

Proposed fix
-const [isRepoPending] = useState<boolean>(false);
+const isRepoPending = false; // Polling removed; kept for TeamInfoSection prop
api/src/auth/auth.controller.ts (3)

35-47: Redundant conditional check after getOrThrow.

getOrThrow throws if the config key is missing, so redirectUrl will always be truthy when line 38 is reached. The if block and the fallback res.json({ token }) on line 47 are unreachable.

Proposed simplification
   githubCallback(`@Req`() req: Request, `@Res`() res: Response) {
     const user: any = (req as any).user;
     const token = this.auth.signToken(user);
     const redirectUrl = this.configService.getOrThrow<string>(
       "OAUTH_SUCCESS_REDIRECT_URL",
     );
-    if (redirectUrl) {
-      res.cookie("token", token, {
-        httpOnly: true,
-        secure: true,
-        sameSite: "none",
-        maxAge: 30 * 24 * 60 * 60 * 1000,
-      });
-      return res.redirect(redirectUrl);
-    }
-    return res.json({ token });
+    res.cookie("token", token, {
+      httpOnly: true,
+      secure: true,
+      sameSite: "none",
+      maxAge: 30 * 24 * 60 * 60 * 1000,
+    });
+    return res.redirect(redirectUrl);
   }

104-114: Use NestJS Logger instead of console.error.

NestJS provides a built-in Logger service that integrates with the framework's logging infrastructure and supports structured logging.

Proposed fix
+import { Logger } from "@nestjs/common";
 ...
 export class AuthController {
+  private readonly logger = new Logger(AuthController.name);
   constructor(
 ...
   } catch (e) {
-    console.error("Error in FortyTwo callback:", e);
+    this.logger.error("Error in FortyTwo callback", e instanceof Error ? e.stack : e);

32-34: Improve type safety for request user extraction.

The double any cast (req as any).user bypasses TypeScript's type checking. Consider using a typed request interface or a custom decorator.

Proposed fix using typed interface
interface AuthenticatedRequest extends Request {
  user: { id: string; [key: string]: any };
}

// Then in methods:
githubCallback(`@Req`() req: AuthenticatedRequest, `@Res`() res: Response) {
  const user = req.user;
  // ...
}

Also applies to: 120-122

frontend/app/events/[id]/my-team/page.tsx (1)

49-54: Consider error handling for fetchQuery.

fetchQuery will throw if the query fails. Without a try/catch, an API error will cause the page to fail entirely. Consider wrapping in try/catch or using prefetchQuery which swallows errors.

Option 1: Use prefetchQuery and handle null team
-const team = await queryClient.fetchQuery({
-  queryKey: myTeamQueryKey(eventId),
-  queryFn: () => myTeamQueryFn(eventId),
-});
+await queryClient.prefetchQuery({
+  queryKey: myTeamQueryKey(eventId),
+  queryFn: () => myTeamQueryFn(eventId),
+});
+const team = queryClient.getQueryData<Team | null>(myTeamQueryKey(eventId));
Option 2: Wrap in try/catch with redirect on error
+let team: Team | null = null;
+try {
   team = await queryClient.fetchQuery({
     queryKey: myTeamQueryKey(eventId),
     queryFn: () => myTeamQueryFn(eventId),
   });
+} catch {
+  redirect(`/events/${eventId}`);
+}

GITHUB_CLIENT_SECRET=
GITHUB_CALLBACK_URL=http://localhost:4000/auth/github/callback
OAUTH_SUCCESS_REDIRECT_URL=http://localhost:3000/auth/sso
42_OAUTH_SUCCESS_REDIRECT_URL=http://localhost:3000/profile
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, check if the file exists and show its content
if [ -f "api/.env.example" ]; then
  echo "=== Content of api/.env.example ==="
  cat -n "api/.env.example"
else
  echo "File api/.env.example not found"
fi

echo ""
echo "=== Searching for 42_OAUTH_SUCCESS_REDIRECT_URL ==="
rg "42_OAUTH_SUCCESS_REDIRECT_URL" || echo "Not found"

echo ""
echo "=== Searching for FORTYTWO_OAUTH_SUCCESS_REDIRECT_URL ==="
rg "FORTYTWO_OAUTH_SUCCESS_REDIRECT_URL" || echo "Not found"

echo ""
echo "=== Searching for any FORTYTWO_ prefixed variables in .env.example ==="
rg "FORTYTWO_" "api/.env.example" || echo "Not found"

Repository: 42core-team/website

Length of output: 1319


🏁 Script executed:

# Check the actual usage in auth.controller.ts to see context
rg -B 2 -A 2 "42_OAUTH_SUCCESS_REDIRECT_URL" "api/src/auth/auth.controller.ts"

Repository: 42core-team/website

Length of output: 179


Invalid environment variable name: starts with a digit. Fix requires changes in both .env.example and code.

42_OAUTH_SUCCESS_REDIRECT_URL is not a valid environment variable name in POSIX-compliant shells because it starts with a digit. This variable is referenced in api/src/auth/auth.controller.ts and must be renamed consistently in both locations.

The codebase already uses the FORTYTWO_* prefix for other 42 School OAuth variables (lines 21-23 in .env.example). Rename the variable to FORTYTWO_OAUTH_SUCCESS_REDIRECT_URL in both:

  1. api/.env.example (line 18)
  2. api/src/auth/auth.controller.ts (in the configService.getOrThrow() call)
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 18-18: [LeadingCharacter] Invalid leading character detected

(LeadingCharacter)


[warning] 18-18: [UnorderedKey] The 42_OAUTH_SUCCESS_REDIRECT_URL key should go before the GITHUB_CALLBACK_URL key

(UnorderedKey)

🤖 Prompt for AI Agents
In `@api/.env.example` at line 18, Rename the invalid environment variable that
starts with a digit: change 42_OAUTH_SUCCESS_REDIRECT_URL to
FORTYTWO_OAUTH_SUCCESS_REDIRECT_URL in the .env.example and update the reference
inside the Auth controller where configService.getOrThrow(...) is called (search
for the string "42_OAUTH_SUCCESS_REDIRECT_URL" in auth.controller.ts) so the
same key is used; also update any other occurrences (tests, docs, or other
config lookups) to the new FORTYTWO_OAUTH_SUCCESS_REDIRECT_URL name to keep
usage consistent.

Comment on lines 43 to 46
tls:
- hosts:
- dev.api.coregame.de
- dev.api.coregame.sh
secretName: dev-api-coregame-de-tls
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TLS secret name references old domain.

The TLS hosts have been updated to dev.api.coregame.sh, but secretName still references the old domain: dev-api-coregame-de-tls.

If the existing secret contains a certificate issued for *.coregame.de, it won't be valid for the new .sh domain, causing TLS errors. Either:

  1. Update the secret name to reflect the new domain and ensure cert-manager generates a new certificate, or
  2. Verify the existing secret already contains a valid certificate for the new domain.
Proposed fix (if creating new secret)
   tls:
     - hosts:
         - dev.api.coregame.sh
-      secretName: dev-api-coregame-de-tls
+      secretName: dev-api-coregame-sh-tls
📝 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
tls:
- hosts:
- dev.api.coregame.de
- dev.api.coregame.sh
secretName: dev-api-coregame-de-tls
tls:
- hosts:
- dev.api.coregame.sh
secretName: dev-api-coregame-sh-tls
🤖 Prompt for AI Agents
In `@api/helm/values-dev.yaml` around lines 43 - 46, The TLS secretName currently
references the old domain (secretName: dev-api-coregame-de-tls) while hosts
lists dev.api.coregame.sh; update the secretName to match the new domain (e.g.,
change secretName to a name reflecting .sh) and ensure cert-manager is
instructed to issue a certificate for dev.api.coregame.sh, or alternatively
confirm the existing secret contains a certificate valid for dev.api.coregame.sh
before leaving secretName unchanged; adjust the tls block's secretName and/or
recreate the TLS secret so the certificate SANs match the hosts entry.

Comment on lines +52 to +75
async upsertSocialAccountForUser(params: {
userId: string;
platform: SocialPlatform;
username: string;
platformUserId: string;
}): Promise<SocialAccountEntity> {
const existing = await this.socialAccountRepository.findOne({
where: { userId: params.userId, platform: params.platform },
});

if (existing) {
existing.username = params.username;
existing.platformUserId = params.platformUserId;
return this.socialAccountRepository.save(existing);
}

const entity = this.socialAccountRepository.create({
userId: params.userId,
platform: params.platform,
username: params.username,
platformUserId: params.platformUserId,
});
return this.socialAccountRepository.save(entity);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f social-account -e ts

Repository: 42core-team/website

Length of output: 230


🏁 Script executed:

find . -name "*social*account*" -type f | grep -E "\.(ts|js|sql)$"

Repository: 42core-team/website

Length of output: 238


🏁 Script executed:

rg "SocialAccountEntity" --type ts -A 5 -B 5

Repository: 42core-team/website

Length of output: 9550


🏁 Script executed:

rg "unique\|constraint" --type ts -i | head -20

Repository: 42core-team/website

Length of output: 45


🏁 Script executed:

cat -n api/src/user/entities/social-account.entity.ts

Repository: 42core-team/website

Length of output: 1269


🏁 Script executed:

rg "upsert" api/src --type ts -i

Repository: 42core-team/website

Length of output: 245


🏁 Script executed:

cat api/package.json | grep -A 2 -B 2 "typeorm"

Repository: 42core-team/website

Length of output: 1224


🏁 Script executed:

rg "Repository.*upsert" --type ts

Repository: 42core-team/website

Length of output: 45


🏁 Script executed:

cat -n api/src/user/social-account.service.ts | head -80

Repository: 42core-team/website

Length of output: 2693


Replace the find-then-save pattern with atomic upsert to prevent constraint violations under concurrent OAuth logins.

The current implementation is race-prone: if two OAuth login requests execute concurrently, both can pass the findOne check, then both attempt to insert. The second request will fail with a unique constraint violation. Although the @Unique(["userId", "platform"]) constraint exists on the entity (preventing actual duplicates), it causes avoidable errors rather than gracefully updating the record.

Use TypeORM's upsert() method keyed on (userId, platform) for atomic conflict resolution:

🛠️ Proposed fix (TypeORM upsert)
-    const existing = await this.socialAccountRepository.findOne({
-      where: { userId: params.userId, platform: params.platform },
-    });
-
-    if (existing) {
-      existing.username = params.username;
-      existing.platformUserId = params.platformUserId;
-      return this.socialAccountRepository.save(existing);
-    }
-
-    const entity = this.socialAccountRepository.create({
-      userId: params.userId,
-      platform: params.platform,
-      username: params.username,
-      platformUserId: params.platformUserId,
-    });
-    return this.socialAccountRepository.save(entity);
+    await this.socialAccountRepository.upsert(
+      {
+        userId: params.userId,
+        platform: params.platform,
+        username: params.username,
+        platformUserId: params.platformUserId,
+      },
+      ["userId", "platform"],
+    );
+    return this.socialAccountRepository.findOne({
+      where: { userId: params.userId, platform: params.platform },
+    });
📝 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
async upsertSocialAccountForUser(params: {
userId: string;
platform: SocialPlatform;
username: string;
platformUserId: string;
}): Promise<SocialAccountEntity> {
const existing = await this.socialAccountRepository.findOne({
where: { userId: params.userId, platform: params.platform },
});
if (existing) {
existing.username = params.username;
existing.platformUserId = params.platformUserId;
return this.socialAccountRepository.save(existing);
}
const entity = this.socialAccountRepository.create({
userId: params.userId,
platform: params.platform,
username: params.username,
platformUserId: params.platformUserId,
});
return this.socialAccountRepository.save(entity);
}
async upsertSocialAccountForUser(params: {
userId: string;
platform: SocialPlatform;
username: string;
platformUserId: string;
}): Promise<SocialAccountEntity> {
await this.socialAccountRepository.upsert(
{
userId: params.userId,
platform: params.platform,
username: params.username,
platformUserId: params.platformUserId,
},
["userId", "platform"],
);
return this.socialAccountRepository.findOne({
where: { userId: params.userId, platform: params.platform },
});
}
🤖 Prompt for AI Agents
In `@api/src/user/social-account.service.ts` around lines 52 - 75, The
upsertSocialAccountForUser implementation is race-prone; replace the
findOne-then-save flow with an atomic upsert using
this.socialAccountRepository.upsert(...) keyed on userId and platform to avoid
unique constraint errors; after calling socialAccountRepository.upsert({ userId,
platform, username, platformUserId }, { conflictPaths: ['userId','platform'] })
fetch and return the persisted entity via this.socialAccountRepository.findOne({
where: { userId: params.userId, platform: params.platform } }) so the method
still returns a SocialAccountEntity.

Comment on lines +9 to +23
useEffect(() => {
let cancelled = false;
(async () => {
try {
await signIn("backend", { redirect: false });
if (!cancelled) {
router.replace("/");
router.refresh();
}
}
catch (e) {
console.error("Failed to finalize SSO:", e);
if (!cancelled)
router.replace("/auth/error");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

NextAuth.js signIn redirect false return value response object

💡 Result:

When you call signIn() with redirect: false (only supported for the Credentials and Email providers), it resolves to a “sign-in response” object shaped like:

type SignInResponse = {
  error: string | undefined
  status: number
  ok: boolean
  url: string | null
}

So you can do:

const res = await signIn("credentials", { redirect: false, ...data })

if (res?.ok) {
  // signed in
  // optionally navigate to res.url (if present)
} else {
  // res?.error contains the error code/message (if any)
}

This behavior (and the exact fields) is documented in the NextAuth.js Client API docs. [1]

Note: there have been reports (notably around NextAuth/Auth.js v5 beta) where signIn(..., { redirect: false }) didn’t return the documented object (e.g., seemingly returning only a URL); see the related GitHub issue. [2]


🏁 Script executed:

# First, let's check the project's dependencies to see NextAuth version
fd -t f "package.json" | head -5 | xargs -I {} grep -l "next-auth" {}

Repository: 42core-team/website

Length of output: 86


🏁 Script executed:

cat frontend/package.json | grep -A5 "next-auth\|auth.js"

Repository: 42core-team/website

Length of output: 245


🏁 Script executed:

# Read the full file to see the context
cat -n frontend/app/auth/sso/page.tsx

Repository: 42core-team/website

Length of output: 1295


Handle non-throwing signIn failures and returned redirect URL.

signIn(..., { redirect: false }) returns a response object with error, status, ok, and url fields rather than throwing on failure. The current code ignores this response, causing failed authentication to still redirect to /, and any provider-specific redirect URL from res.url is ignored.

✅ Proposed fix
-        await signIn("backend", { redirect: false });
-        if (!cancelled) {
-          router.replace("/");
-          router.refresh();
-        }
+        const res = await signIn("backend", { redirect: false });
+        if (cancelled) return;
+        if (!res || res.error) {
+          router.replace("/auth/error");
+          return;
+        }
+        const target = res.url ?? "/";
+        router.replace(target);
+        router.refresh();
📝 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
useEffect(() => {
let cancelled = false;
(async () => {
try {
await signIn("backend", { redirect: false });
if (!cancelled) {
router.replace("/");
router.refresh();
}
}
catch (e) {
console.error("Failed to finalize SSO:", e);
if (!cancelled)
router.replace("/auth/error");
}
useEffect(() => {
let cancelled = false;
(async () => {
try {
const res = await signIn("backend", { redirect: false });
if (cancelled) return;
if (!res || res.error) {
router.replace("/auth/error");
return;
}
const target = res.url ?? "/";
router.replace(target);
router.refresh();
}
catch (e) {
console.error("Failed to finalize SSO:", e);
if (!cancelled)
router.replace("/auth/error");
}
})();
return () => {
cancelled = true;
};
});
🤖 Prompt for AI Agents
In `@frontend/app/auth/sso/page.tsx` around lines 9 - 23, The async sign-in flow
currently ignores the response from signIn and treats failures the same as
successes; update the useEffect handler (the async IIFE that calls signIn) to
await and inspect the returned response object from signIn("backend", {
redirect: false }) instead of assuming it throws: if res.error or !res.ok then,
when not cancelled, call router.replace("/auth/error"); else if res.url is
present then navigate to the provider redirect (e.g., set window.location.href =
res.url) instead of router.replace("/"), and only call router.replace("/") and
router.refresh() when res.ok is true and no external res.url exists; keep the
try/catch to log real exceptions.

Comment on lines +27 to +37
const { data: team } = useQuery<Team | null>({
refetchInterval: 3000,
queryKey: ["event", eventId, "my-team"],
queryFn: async () => {
const response = await axiosInstance.get<Team | null>(
`/team/event/${eventId}/my`,
);
return response.data;
},
initialData: initialTeam,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the queries file and check the myTeamQueryKey definition
fd -type f -name "queries*" frontend/

Repository: 42core-team/website

Length of output: 233


🏁 Script executed:

# Also check the TeamInfoDisplay.tsx file to understand context
head -100 frontend/app/events/[id]/my-team/components/TeamInfoDisplay.tsx

Repository: 42core-team/website

Length of output: 2645


🏁 Script executed:

# Find and inspect page.tsx in the same directory
fd -type f -name "page.tsx" frontend/app/events/[id]/my-team/

Repository: 42core-team/website

Length of output: 233


🏁 Script executed:

# Find queries file in the frontend directory
find frontend -name "*queries*" -type f

Repository: 42core-team/website

Length of output: 148


🏁 Script executed:

# Find page.tsx in the my-team directory structure
find frontend/app/events/*/my-team -name "page.tsx" -type f

Repository: 42core-team/website

Length of output: 104


🏁 Script executed:

# Search for myTeamQueryKey definition anywhere
rg "myTeamQueryKey" frontend/

Repository: 42core-team/website

Length of output: 446


🏁 Script executed:

# Get the full definition of myTeamQueryKey and teamMembersQueryKey
cat frontend/app/events/[id]/my-team/queries.ts

Repository: 42core-team/website

Length of output: 1059


🏁 Script executed:

# Check what page.tsx is doing with these keys
cat frontend/app/events/[id]/my-team/page.tsx

Repository: 42core-team/website

Length of output: 2008


🏁 Script executed:

# Check leaveTeam function to understand expected behavior
rg "export.*leaveTeam" frontend/app/actions/team.ts -A 10

Repository: 42core-team/website

Length of output: 364


🏁 Script executed:

# Check if there's any redirect or side effect after leaveTeam in other files
rg "leaveTeam" frontend/app/events/\[id\]/my-team/ -B 2 -A 5

Repository: 42core-team/website

Length of output: 3140


🏁 Script executed:

# Check if there's any handling in teamView.tsx or elsewhere after leaveTeam
cat frontend/app/events/[id]/my-team/teamView.tsx | head -80

Repository: 42core-team/website

Length of output: 2368


Add onSuccess handler to leaveTeamMutation to invalidate cached queries.

After successfully leaving the team, the cached team and pending invites data should be invalidated to ensure the UI reflects the updated state. Currently, the mutation lacks an onSuccess handler, which means stale data may persist until the 3-second refetch interval triggers.

Also, remove the unused isRepoPending state (line 24) — it's initialized to false and never modified, yet passed to TeamInfoSection.

Suggested fix
+import { myTeamQueryKey, pendingInvitesQueryKey } from "../queries";
+import { useQueryClient } from "@tanstack/react-query";
...
+const queryClient = useQueryClient();
...
 const leaveTeamMutation = useMutation({
   mutationFn: async () => {
     const result = await leaveTeam(eventId);
     if (isActionError(result)) {
       throw new Error(result.error);
     }
   },
   onMutate: () => {
     plausible("leave_team");
     setErrorMessage(null);
   },
+  onSuccess: () => {
+    queryClient.invalidateQueries({ queryKey: myTeamQueryKey(eventId) });
+    queryClient.invalidateQueries({ queryKey: pendingInvitesQueryKey(eventId) });
+  },
   onError: (error: Error) => {
     setErrorMessage(error.message);
   },
 });
-const [errorMessage, setErrorMessage] = useState<string | null>(null);
-const [isRepoPending] = useState<boolean>(false);
+const [errorMessage, setErrorMessage] = useState<string | null>(null);
-      isRepoPending={isRepoPending}
🤖 Prompt for AI Agents
In `@frontend/app/events/`[id]/my-team/components/TeamInfoDisplay.tsx around lines
27 - 37, Add an onSuccess handler to the leaveTeamMutation so that after a
successful API call you invalidate the cached queries for the current event's
team and pending invites (e.g., invalidateQueries(["event", eventId, "my-team"])
and invalidateQueries(["event", eventId, "pending-invites"]) via the react-query
QueryClient) to ensure the UI updates immediately; also remove the unused
isRepoPending state and stop passing it into TeamInfoSection (delete its
declaration and the prop usage). Use the existing eventId and leaveTeamMutation
identifiers to locate where to attach the onSuccess and call
queryClient.invalidateQueries.

Comment on lines 63 to +79
useEffect(() => {
async function fetchQueueState() {
const newQueueState = await getQueueState(props.eventId);
if (
queueState.match?.state === MatchState.IN_PROGRESS
&& newQueueState.match?.state !== MatchState.IN_PROGRESS
) {
if (newQueueState.match) {
router.push(`/events/${eventId}/match/${newQueueState?.match?.id}`);
}
}
setQueueState(newQueueState);
if (!queueState) {
return;
}

const interval = setInterval(fetchQueueState, 600);
return () => clearInterval(interval);
});
if (queueState.match?.state === MatchState.IN_PROGRESS) {
return;
}

const previousMatchState = props.queueState.match?.state;
if (
previousMatchState === MatchState.IN_PROGRESS
&& queueState.match
) {
router.push(`/events/${eventId}/match/${queueState.match.id}`);
}
}, [queueState, router, eventId, props.queueState.match?.state]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "queueState.tsx" | head -20

Repository: 42core-team/website

Length of output: 110


🏁 Script executed:

cat -n ./frontend/app/events/[id]/queue/queueState.tsx

Repository: 42core-team/website

Length of output: 5202


🏁 Script executed:

cat -n ./frontend/app/events/[id]/queue/queueState.tsx | sed -n '35,79p'

Repository: 42core-team/website

Length of output: 1570


🏁 Script executed:

rg -l "QueueState" --type tsx --type ts | grep -v node_modules | head -20

Repository: 42core-team/website

Length of output: 90


🏁 Script executed:

rg -l "QueueState" --type ts | head -20

Repository: 42core-team/website

Length of output: 315


🏁 Script executed:

cat -n ./frontend/app/events/[id]/queue/page.tsx

Repository: 42core-team/website

Length of output: 3589


Track previous match state with useRef instead of reading from static props.

The redirect logic has a critical flaw: props.queueState is set once during server-side rendering and never updates in the client. The queueState variable (from useQuery with refetchInterval: 600) updates regularly, but the redirect compares its state transition against a static previous value from props. This means transitions detected after the initial render won't trigger the redirect.

Use a ref to track actual previous state across all queueState updates:

🔧 Proposed fix
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
@@
   const router = useRouter();
   const { id } = useParams();
   const eventId = id as string;
+  const previousMatchStateRef = useRef<MatchState | undefined>(
+    props.queueState.match?.state,
+  );
@@
   useEffect(() => {
     if (!queueState) {
       return;
     }
 
-    if (queueState.match?.state === MatchState.IN_PROGRESS) {
-      return;
-    }
-
-    const previousMatchState = props.queueState.match?.state;
-    if (
-    previousMatchState === MatchState.IN_PROGRESS
-      && queueState.match
-    ) {
-      router.push(`/events/${eventId}/match/${queueState.match.id}`);
-    }
-  }, [queueState, router, eventId, props.queueState.match?.state]);
+    const previousMatchState = previousMatchStateRef.current;
+    if (
+      previousMatchState === MatchState.IN_PROGRESS &&
+      queueState.match &&
+      queueState.match.state !== MatchState.IN_PROGRESS
+    ) {
+      router.push(`/events/${eventId}/match/${queueState.match.id}`);
+    }
+
+    previousMatchStateRef.current = queueState.match?.state;
+  }, [queueState, router, eventId]);
🤖 Prompt for AI Agents
In `@frontend/app/events/`[id]/queue/queueState.tsx around lines 63 - 79, The
redirect compares the live queueState to the static props.queueState, so it
never detects client-side transitions; replace the props-based previous state
with a ref: create a prevMatchStateRef via useRef, initialize it to
props.queueState.match?.state, then in the useEffect that watches queueState
(and router/eventId) compare prevMatchStateRef.current to
queueState.match?.state (checking for transition from MatchState.IN_PROGRESS to
a new match), call router.push when appropriate, and finally update
prevMatchStateRef.current = queueState.match?.state; ensure you remove
props.queueState.match?.state from the effect deps and reference the symbols
queueState, props.queueState, prevMatchStateRef, useEffect,
MatchState.IN_PROGRESS, and router.push to locate the code.

Comment on lines +52 to 54
<Link color="primary" href="/events/create">
Create Event
</Link>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/app/events/page.tsx | head -60

Repository: 42core-team/website

Length of output: 2201


🏁 Script executed:

# Check imports at the top of the file
head -20 frontend/app/events/page.tsx

Repository: 42core-team/website

Length of output: 667


🏁 Script executed:

# Get a broader view around lines 52-54
sed -n '40,65p' frontend/app/events/page.tsx

Repository: 42core-team/website

Length of output: 808


🏁 Script executed:

# Check package.json for UI libraries
cat package.json | grep -A 50 '"dependencies"'

Repository: 42core-team/website

Length of output: 107


🏁 Script executed:

# Search for other Link usages to see if there's a pattern
rg '<Link' frontend/app -A 2 -B 1 | head -50

Repository: 42core-team/website

Length of output: 2606


🏁 Script executed:

# Find package.json
fd package.json

Repository: 42core-team/website

Length of output: 129


🏁 Script executed:

# Check if there are UI library imports elsewhere
rg "from.*ui|from.*nextui|from.*shadcn" frontend/app --no-heading | head -20

Repository: 42core-team/website

Length of output: 1784


🏁 Script executed:

# Check frontend package.json for dependencies
cat frontend/package.json | grep -A 100 '"dependencies"'

Repository: 42core-team/website

Length of output: 2559


🏁 Script executed:

# Check if there's a Link component in the UI components folder
fd Link frontend/components/ui

Repository: 42core-team/website

Length of output: 45


🏁 Script executed:

# Check the components/ui directory structure
ls -la frontend/components/ui/ | head -30

Repository: 42core-team/website

Length of output: 1577


🏁 Script executed:

# Check if there's a custom Link component anywhere
rg "export.*Link|export const Link" frontend/components

Repository: 42core-team/website

Length of output: 45


🏁 Script executed:

# Check the button component to see if it wraps Link
cat frontend/components/ui/button.tsx

Repository: 42core-team/website

Length of output: 2018


🏁 Script executed:

# Check the TypeScript definitions for next/link to confirm color prop doesn't exist
rg "type LinkProps|interface LinkProps" --type ts frontend/node_modules/next/dist/client

Repository: 42core-team/website

Length of output: 141


🏁 Script executed:

# Verify the pattern: check if other Link uses className instead
rg '<Link' frontend/app --no-heading -A 1 | grep -E "(className|color)"

Repository: 42core-team/website

Length of output: 146


🏁 Script executed:

# Double-check: search for any TypeScript errors or compiler output related to this
rg "color.*prop|prop.*color" frontend/app/events/page.tsx

Repository: 42core-team/website

Length of output: 45


Remove unsupported color prop from next/link.

The color prop is not valid for next/link. Use className with Tailwind classes instead (e.g., className="text-primary").

Suggested fix
-          <Link color="primary" href="/events/create">
+          <Link href="/events/create" className="text-primary">
             Create Event
           </Link>
📝 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
<Link color="primary" href="/events/create">
Create Event
</Link>
<Link href="/events/create" className="text-primary">
Create Event
</Link>
🤖 Prompt for AI Agents
In `@frontend/app/events/page.tsx` around lines 52 - 54, The Link component usage
in events/page.tsx incorrectly passes a non-existent color prop to next/link;
remove the color="primary" prop from the <Link> element and instead add a
Tailwind class via className (for example className="text-primary" or whatever
design token you use) to style the link; update the <Link> with className and
ensure any nested children receive the styling as needed (reference the Link
element in this file).

Comment on lines 6 to 14
const queryClient = new QueryClient();

export default function Providers({ children }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

TanStack React Query Next.js App Router QueryClient initialization best practices

💡 Result:

Best practices (Next.js App Router + TanStack React Query)

1) Put QueryClientProvider in a Client Component “Providers” wrapper

React context/providers aren’t supported in Server Components, so create a dedicated client wrapper (e.g. app/providers.tsx) and import it from app/layout.tsx. [3]

2) Create the QueryClient per request on the server, but as a stable singleton in the browser

TanStack’s App Router guidance is:

  • Server: always make a new QueryClient (prevents cross-request/user cache sharing).
  • Browser: reuse one QueryClient instance (prevents recreating the client on re-renders / suspense). [1]
// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider, isServer } from '@tanstack/react-query'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000, // helps avoid immediate refetch after SSR
      },
    },
  })
}

let browserQueryClient: QueryClient | undefined

function getQueryClient() {
  if (isServer) return makeQueryClient()
  if (!browserQueryClient) browserQueryClient = makeQueryClient()
  return browserQueryClient
}

export function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient()
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}

3) For SSR/prefetch: prefetch in Server Components, then dehydrate<HydrationBoundary>

In the App Router model, treat Server Components as a “prefetching/loading phase”: create a fresh QueryClient, prefetchQuery, then pass the dehydrated cache into a <HydrationBoundary> that wraps the Client Component(s) that call useQuery. [1][2]

4) Mind server memory: clear or tune gcTime when you create per-request clients

If you’re creating a QueryClient per request, TanStack notes you may need to queryClient.clear() after dehydration (or adjust gcTime carefully; avoid gcTime: 0 because it can cause hydration issues). [4]

Sources: TanStack Advanced SSR (Next.js App Router) [1], TanStack SSR & Hydration guide [2], Next.js App Router composition/context provider pattern [3], TanStack SSR memory/gcTime notes [4].


🌐 Web query:

React Query module scope singleton state leakage Next.js SSR

💡 Result:

In Next.js SSR, anything you create at module scope (file top-level) in a Node.js server process can be reused across requests. If you do this with React Query / TanStack Query:

// ❌ shared across all SSR requests in the same server process
export const queryClient = new QueryClient()

…then the QueryClient cache becomes a singleton, so request A can populate the cache and request B can read/serialize it, causing cross-user data leakage and also unnecessary cache growth. TanStack Query explicitly warns against creating the QueryClient at the file root for SSR because it makes the cache shared “between all requests” and can leak sensitive data. [1]

Fix patterns (safe)

1) Pages Router (pages/): create it inside _app (not module scope)

Create the client in React state (or a ref) so it’s per request on the server and stable per browser session on the client. [1]

// pages/_app.tsx
import * as React from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

export default function MyApp({ Component, pageProps }) {
  const [queryClient] = React.useState(() => new QueryClient())
  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
    </QueryClientProvider>
  )
}

2) App Router (app/) with Server Components: use a request-scoped singleton via cache()

If you want “singleton-like” reuse within the same request (but not across requests), TanStack Query recommends using React’s cache() (scoped per request) in Next.js App Router. [2]

// app/getQueryClient.ts
import { QueryClient } from '@tanstack/react-query'
import { cache } from 'react'

// ✅ request-scoped, avoids cross-request leakage
export const getQueryClient = cache(() => new QueryClient())

Then prefetch/dehydrate using that request-scoped client. [2]


Rule of thumb: in SSR, never put new QueryClient() at module scope. Create it per request (or request-scoped via cache() in App Router). [1][2]

Sources: [1] [2]


🌐 Web query:

React Query useState QueryClient initializer Next.js

💡 Result:

In Next.js, create the QueryClient inside a Client Component and keep it stable across re-renders by using a lazy initializer (or a “getQueryClient” helper). Do not create it at module scope (const queryClient = new QueryClient()), especially with SSR, because it can share cache across requests/users. [1]

App Router (app/) – recommended (TanStack Query v5)

TanStack’s SSR guide shows a getQueryClient() approach that creates a new client on the server, but reuses a singleton in the browser (and notes a Suspense-related caveat about useState). [2]

// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider, isServer } from '@tanstack/react-query'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: { queries: { staleTime: 60_000 } },
  })
}

let browserQueryClient: QueryClient | undefined

function getQueryClient() {
  if (isServer) return makeQueryClient()
  if (!browserQueryClient) browserQueryClient = makeQueryClient()
  return browserQueryClient
}

export function Providers({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient()
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}

Then wrap your layout:

// app/layout.tsx
import { Providers } from './providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body><Providers>{children}</Providers></body>
    </html>
  )
}

Pages Router (pages/) – common pattern

Use React state with a lazy initializer so the client is created once per app/request lifecycle (and not recreated on every render). [1]

// pages/_app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import * as React from 'react'

export default function MyApp({ Component, pageProps }) {
  const [queryClient] = React.useState(
    () => new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } })
  )

  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
    </QueryClientProvider>
  )
}

Why useState(() => new QueryClient())? It lazily constructs the client once and keeps the same instance across re-renders, preventing cache resets and avoiding shared caches across SSR requests when done in the right place. [1]

Sources: TanStack Query “Server Rendering & Hydration” (v5) and “Advanced SSR” guide. [1] [2]


QueryClient instantiation at module scope causes cross-user cache leakage in Next.js App Router.

Creating QueryClient at module level is unsafe in Next.js SSR. The same instance is reused across all server requests, allowing one user's cached data to be served to another user.

TanStack Query's recommended pattern for App Router is to create a fresh QueryClient on the server per request, but maintain a stable singleton in the browser. Use the isServer helper with a getQueryClient() function rather than useState:

Proposed fix
 "use client";

 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { isServer } from "@tanstack/react-query";
 import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

-const queryClient = new QueryClient();
+function makeQueryClient() {
+  return new QueryClient({
+    defaultOptions: {
+      queries: {
+        staleTime: 60 * 1000,
+      },
+    },
+  });
+}
+
+let browserQueryClient: QueryClient | undefined;
+
+function getQueryClient() {
+  if (isServer) return makeQueryClient();
+  if (!browserQueryClient) browserQueryClient = makeQueryClient();
+  return browserQueryClient;
+}

-export default function Providers({ children }) {
+export default function Providers({
+  children,
+}: {
+  children: React.ReactNode;
+}) {
+  const queryClient = getQueryClient();
+
   return (
     <QueryClientProvider client={queryClient}>
       {children}
       <ReactQueryDevtools initialIsOpen={false} />
     </QueryClientProvider>
   );
 }
🤖 Prompt for AI Agents
In `@frontend/app/providers.tsx` around lines 6 - 14, The module-level QueryClient
instance (const queryClient) in Providers leads to cross-request cache leakage;
remove that top-level instantiation and implement a getQueryClient() helper that
returns a new QueryClient when isServer() is true (per-request SSR) and a stable
singleton in the browser, then update the Providers component to call
getQueryClient() and pass its result to <QueryClientProvider client={...}> while
preserving ReactQueryDevtools and children; ensure the unique symbols referenced
are QueryClient, Providers, QueryClientProvider, getQueryClient, and isServer.

Comment on lines +58 to +73
try {
const res = await axiosInstance.get<{
id: string;
username: string;
email: string;
profilePicture: string;
}>(`/auth/me`);

if (dbUser)
session.user.id = dbUser.id;
session.user.id = res.data.id;
session.user.email = res.data.email;
session.user.name = res.data.username;
session.user.profilePicture = res.data.profilePicture;
}
catch {
session.user.id = "";
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/app/utils/authOptions.ts | head -100

Repository: 42core-team/website

Length of output: 2572


🏁 Script executed:

rg "useSession\|NextAuthOptions\|session.*callback" frontend/app/utils/authOptions.ts -A 5 -B 5

Repository: 42core-team/website

Length of output: 45


🌐 Web query:

NextAuth.js v4 session callback return null unauthenticated

💡 Result:

In NextAuth.js v4, the callbacks.session callback is expected to always return a session object (e.g. return session). The official docs only describe returning a session object, not null. [1]

If you want a request to be treated as unauthenticated, don’t try to return null from session(). There’s an (older) feature request asking for this (“Allow the session callback to return null”) and the suggested workaround is to return a session with a flag (e.g. isAuthenticated: false) and then convert it to null at the call site (e.g. after getServerSession). [2]

Common related gotcha: if you’re seeing getServerSession() (or /api/auth/session) always return null, make sure you have NEXTAUTH_SECRET (or an explicit JWT secret) set; otherwise session decryption can fail and you’ll effectively get null sessions. [3]

Sources:
[1] NextAuth.js v4 callbacks docs (session callback examples) (next-auth.js.org)
[2] GitHub issue: “Allow the session callback to return null” (#7035) (github.com)
[3] GitHub discussion: getServerSession always null; fix by setting NEXTAUTH_SECRET (github.com)

Citations:


Clear all session user fields on /auth/me error to prevent stale data.

On error, only session.user.id is cleared while name/email/profilePicture remain from the JWT, causing useSession() to report authenticated with outdated information. NextAuth.js v4 session callbacks cannot return null to mark a user unauthenticated (see next-auth.js.org/configuration/callbacks). Instead, clear all affected fields on error:

catch {
-  session.user.id = "";
+  session.user.id = "";
+  session.user.email = "";
+  session.user.name = "";
+  session.user.profilePicture = "";
}

Alternatively, set an isAuthenticated flag to false and handle it at the call site to properly treat the session as unauthenticated.

🤖 Prompt for AI Agents
In `@frontend/app/utils/authOptions.ts` around lines 58 - 73, On failure of the
axiosInstance.get('/auth/me') call, clear all session user fields instead of
only session.user.id to avoid stale JWT-derived data; in the catch block set
session.user.id, session.user.name, session.user.email, and
session.user.profilePicture to empty values (or add/set an explicit
session.user.isAuthenticated = false and handle that at the call site) so
useSession() no longer reports an authenticated user with outdated information.

Comment on lines +7 to 10
const base
= process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL;
window.location.href = `${base?.replace(/\/$/, "")}/auth/github/callback`;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against missing backend URL to avoid broken redirects.

If NEXT_PUBLIC_BACKEND_PUBLIC_URL is unset, Line 9 builds a undefined/auth/... path and silently redirects to a bad URL. Fail fast and log clearly.

✅ Suggested fix
-      const base
-        = process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL;
-      window.location.href = `${base?.replace(/\/$/, "")}/auth/github/callback`;
+      const base = process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL?.replace(/\/$/, "");
+      if (!base) {
+        console.error("NEXT_PUBLIC_BACKEND_PUBLIC_URL is not set");
+        return;
+      }
+      window.location.href = `${base}/auth/github/callback`;
📝 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 base
= process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL;
window.location.href = `${base?.replace(/\/$/, "")}/auth/github/callback`;
}
const base = process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL?.replace(/\/$/, "");
if (!base) {
console.error("NEXT_PUBLIC_BACKEND_PUBLIC_URL is not set");
return;
}
window.location.href = `${base}/auth/github/callback`;
}
🤖 Prompt for AI Agents
In `@frontend/components/github.tsx` around lines 7 - 10, The redirect builds a
broken URL when process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL is missing; update
the logic around the code that reads NEXT_PUBLIC_BACKEND_PUBLIC_URL and sets
window.location.href (the block using base =
process.env.NEXT_PUBLIC_BACKEND_PUBLIC_URL and then window.location.href =
`${base?.replace(/\/$/, "")}/auth/github/callback`) to validate base first, and
if it's falsy, log a clear error (or surface a UI error) and do not perform the
redirect; only call replace(...) and assign window.location.href when base is a
non-empty string.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (4)
frontend/components/event-navbar.tsx (1)

35-64: Nav items won’t update after the start time passes

If a user lands before the start time and stays on the page, hasStarted won’t flip until another re-render happens, so “Group Phase / Tournament Tree” can remain hidden. Consider a timed state update or server-provided started flag to keep the UI accurate.

🔧 Suggested fix (timer-based update)
-import { useMemo } from "react";
+import { useEffect, useMemo, useState } from "react";
@@
-  const hasStarted = Date.now() >= new Date(event.startDate).getTime();
+  const startTs = new Date(event.startDate).getTime();
+  const [hasStarted, setHasStarted] = useState(() => Date.now() >= startTs);
+
+  useEffect(() => {
+    if (!Number.isFinite(startTs)) return;
+    if (Date.now() >= startTs) {
+      setHasStarted(true);
+      return;
+    }
+    const id = setTimeout(() => setHasStarted(true), startTs - Date.now());
+    return () => clearTimeout(id);
+  }, [startTs]);
github-service/src/repo.utils.ts (1)

266-276: Bug: Duplicated folder path in updateTeamName.

The repoRoot parameter already includes MY_CORE_BOT_FOLDER (set at line 48 via path.join(tempFolderPath, this.MY_CORE_BOT_FOLDER)), but line 271-273 joins it again with this.MY_CORE_BOT_FOLDER. This results in an invalid path like bots/softcore/bots/softcore/src/main.c.

🐛 Proposed fix
   private async updateTeamName(
     repoRoot: string,
     teamName: string,
   ): Promise<void> {
     try {
       const mainCPath = path.join(
         repoRoot,
-        this.MY_CORE_BOT_FOLDER,
         "src",
         "main.c",
       );
github-service/src/app.service.ts (2)

264-279: Critical: Error swallowed after repository deletion causes inconsistent state.

When cloning/pushing fails (lines 264-274), the error is caught, logged, and the repository is deleted, but execution continues without re-throwing. This causes:

  1. The repository_created event to be emitted (lines 276-279) even though creation failed
  2. Subsequent collaborator operations (lines 281-302) to fail on a non-existent repository

The comment states "Error is handled by the outer catch block" but the error is not re-thrown, so the outer catch never executes.

🐛 Proposed fix
       } catch (e) {
         this.logger.error(
           `Failed to clone mono repo and push to team repo for repo ${name}`,
           e as Error,
         );
         await this.deleteRepository(name, githubOrg, secret);
-        // Error is handled by the outer catch block; do not re-throw here.
+        throw e; // Re-throw to prevent emitting repository_created and adding collaborators
       } finally {
         await fs.rm(tempFolderPath, { recursive: true, force: true });
         this.logger.log(`Removed temp folder ${tempFolderPath}`);
       }

26-31: Incorrect error code check: should be "EEXIST" not "EXIST".

Node.js uses "EEXIST" for the error code when a file/directory already exists. The current condition error.code !== "EXIST" will always be true for this case, making the warning log fire for the wrong reason.

🐛 Proposed fix
       .catch((error) => {
-        if (error.code !== "EXIST") {
+        if (error.code === "EEXIST") {
           this.logger.warn(
             `Failed to create temp folder at ${this.TMP_FOLDER} because it already exists`,
           );
+        } else {
+          this.logger.error(
+            `Failed to create temp folder at ${this.TMP_FOLDER}`,
+            error,
+          );
         }
       });
🤖 Fix all issues with AI agents
In `@api/src/auth/auth.controller.ts`:
- Around line 110-114: The thrown BadRequestException in auth.controller.ts
currently interpolates e.message and may leak internal error details; change the
throw to use a generic message (e.g., "Invalid state parameter.") and remove
inclusion of e.message while leaving the existing debug log at the earlier line
intact; target the BadRequestException throw that references the variable e in
the relevant method of the AuthController to make this change.
- Around line 35-47: The code in AuthController where redirectUrl is assigned
via this.configService.getOrThrow<string>("OAUTH_SUCCESS_REDIRECT_URL") is
checking if (redirectUrl) even though getOrThrow will always return or throw,
making the conditional dead code; either remove the if and always set the cookie
+ res.redirect(redirectUrl) (if redirect is mandatory), or change getOrThrow to
this.configService.get<string>("OAUTH_SUCCESS_REDIRECT_URL") and keep the if to
allow fallback to return res.json({ token }) when redirectUrl is undefined —
update the block in auth.controller.ts (the redirectUrl variable and surrounding
cookie/redirect/JSON logic) accordingly.

In `@api/src/auth/github.strategy.ts`:
- Around line 51-60: The new-user creation passes profilePicture from the GitHub
profile which may be undefined; update the call to users.createUser in
github.strategy.ts (the block that constructs new user via
this.users.createUser) to provide a safe default (e.g., empty string or a
configured placeholder URL) using a nullish fallback for profilePicture so the
database always receives a string and downstream code won’t break.

In `@api/src/constants.ts`:
- Around line 1-4: Replace the hardcoded lock key literal 12346 in
match.service.ts with the enum value LockKeys.CREATE_TEAM_REPOS; locate where
lockKey is assigned (the const lockKey = ... usage) and change it to use
LockKeys.CREATE_TEAM_REPOS, and ensure LockKeys is imported from the constants
module (add or update the import for LockKeys if missing).

In `@api/src/team/team.service.ts`:
- Line 149: The repoName construction using team.event.name + "-" + team.name +
"-" + team.id must be sanitized for GitHub: create (or call) a sanitizeRepoName
utility and replace/normalize characters from team.event.name and team.name
(e.g., remove/replace non-alphanumeric characters with hyphens or underscores,
convert to lowercase, collapse consecutive hyphens, trim leading/trailing
hyphens, and enforce a sane length), then build repoName =
`${sanitize(part1)}-${sanitize(part2)}-${team.id}`; update the code around the
repoName assignment and export the sanitize function for reuse.
- Around line 175-190: The outer createTeam wraps work in dataSource.transaction
but then calls createTeamRepository which starts its own independent
transaction, risking inconsistent side-effects if the outer transaction rolls
back; update createTeamRepository to accept an optional EntityManager (or
TransactionalRepository) parameter and, inside createTeam, pass the transaction
manager (the value returned to the transaction callback) when calling
createTeamRepository (use eventService.hasEventStarted(eventId) as before, then
await createTeamRepository(newTeam.id, manager) so both DB operations share the
same transaction), or alternatively remove the createTeamRepository call from
inside the transaction and invoke it after the transaction completes; adjust
function signatures accordingly (createTeamRepository(..., manager?:
EntityManager)).
- Around line 44-49: The current use of teamRepository.findBy cannot filter on
nested relation fields or include relations; replace the findBy call with
teamRepository.find({ relations: { event: true }, where: {
startedRepoCreationAt: IsNull(), event: { startDate: LessThanOrEqual(new Date())
} } }) or implement the equivalent QueryBuilder query so the event relation is
joined and the nested condition on event.startDate is applied; update any
references to the old findBy invocation accordingly and keep the predicates
startedRepoCreationAt, event.startDate and LessThanOrEqual identical.

In `@frontend/app/events/EventTable.tsx`:
- Line 25: Remove the debug console.log call that's printing every event in the
EventTable component: delete the line console.log("try to format state for
event", event) (or replace it with a development-only guard like if
(process.env.NODE_ENV !== 'production') { /* debug log */ }) so the EventTable
rendering no longer spams the browser console; locate the statement inside the
EventTable component where the event is being formatted and remove or
conditionally guard it.
- Around line 35-45: Replace the existing status logic that checks only
event.currentRound with logic that uses both event.currentRound and
event.endDate: treat event.currentRound === 0 as the "Initial"/not-started
state, treat the event as "Completed" only when new Date() is after new
Date(event.endDate), and treat event.currentRound > 0 with endDate in the future
as "In Progress"; update the return objects (text and variant) accordingly where
the current snippet lives (EventTable.tsx) using the event.currentRound and
event.endDate fields.

In `@frontend/components/event-info-notice.tsx`:
- Around line 19-32: The refresh guard didRefreshRef is never reset when a new
event (startsAt) is loaded, so add logic to reset it whenever startsAt changes;
specifically, add a small useEffect that watches startsAt and sets
didRefreshRef.current = false (referencing didRefreshRef and startsAt) so the
countdown can trigger router.refresh() for the new event.
🧹 Nitpick comments (6)
api/src/match/match.service.ts (1)

50-52: Use LockKeys.CREATE_TEAM_REPOS instead of hardcoded value.

A new LockKeys enum was introduced in api/src/constants.ts specifically to centralize advisory lock keys. This hardcoded 12346 should use LockKeys.CREATE_TEAM_REPOS for consistency and maintainability.

♻️ Proposed fix
+import { LockKeys } from "../constants";
+
 `@Cron`(CronExpression.EVERY_5_SECONDS)
 async processQueueMatches() {
-  const lockKey = 12346;
+  const lockKey = LockKeys.CREATE_TEAM_REPOS;
   const queryRunner = this.dataSource.createQueryRunner();
api/src/user/user.service.ts (1)

123-128: Consider reusing the existing EventService.isEventPublic method.

The isEventPublic check duplicates logic already available in EventService.isEventPublic (as shown in the relevant code snippets). This creates maintenance burden if the visibility logic changes.

♻️ Suggested refactor to reuse existing service method

Inject EventService and call the existing method:

-    const isEventPublic = await this.userRepository.manager
-      .getRepository(EventEntity)
-      .existsBy({
-        id: eventId,
-        isPrivate: false,
-      });
+    const isEventPublic = await this.eventService.isEventPublic(eventId);

This requires adding EventService to the constructor dependencies.

frontend/app/events/EventTable.tsx (1)

83-87: Avoid calling formatState twice per event row.

formatState(event) is invoked twice for each event, computing the same result. This is inefficient, especially with the current console.log inside.

♻️ Suggested improvement
             events.map((event) => (
-              <TableRow
+              const state = formatState(event);
+              return <TableRow
                 key={event.id}
                 className="cursor-pointer transition-colors hover:bg-muted/50"
                 onClick={() => router.push(`/events/${event.id}`)}
               >
                 <TableCell className="font-medium">{event.name}</TableCell>
                 <TableCell>
                   {new Date(event.startDate).toLocaleDateString()}
                 </TableCell>
                 <TableCell>
                   {event.minTeamSize} -{event.maxTeamSize} members
                 </TableCell>
                 <TableCell>
-                  <Badge variant={formatState(event).variant}>
-                    {formatState(event).text}
+                  <Badge variant={state.variant}>
+                    {state.text}
                   </Badge>
                 </TableCell>
-              </TableRow>
+              </TableRow>;
             ))

Or wrap formatState with useCallback if it needs to be memoized at the component level.

api/src/auth/auth.controller.ts (1)

32-34: Improve type safety for request user access.

Using (req as any).user bypasses TypeScript checking. Consider defining a typed request interface or using the existing @UserId() decorator consistently.

♻️ Suggested approach

Define a typed request interface:

interface AuthenticatedRequest extends Request {
  user: { id: string; /* other fields */ };
}

Then use it:

- githubCallback(`@Req`() req: Request, `@Res`() res: Response) {
-   const user: any = (req as any).user;
+ githubCallback(`@Req`() req: AuthenticatedRequest, `@Res`() res: Response) {
+   const user = req.user;

Or for /me, simply use the existing decorator:

- me(`@Req`() req: Request) {
-   const user: any = (req as any).user;
-   return this.userService.getUserById(user.id);
+ me(`@UserId`() userId: string) {
+   return this.userService.getUserById(userId);

Also applies to: 120-122

github-service/src/app.service.ts (1)

281-303: Consider handling partial failures when adding collaborators.

Using Promise.all means if one user fails to be added, the entire operation rejects immediately. Other users may be in an inconsistent state (some added, some not).

Consider Promise.allSettled with error aggregation if partial success should be logged and handled gracefully, or ensure retries/cleanup logic exists upstream.

♻️ Alternative using Promise.allSettled
-      await Promise.all(
-        githubUsers.map(async (user) => {
+      const results = await Promise.allSettled(
+        githubUsers.map(async (user) => {
           const { username, githubAccessToken } = user;
           this.logger.log(
             `Adding user ${username} to repository ${name} in org ${githubOrg}`,
           );
           await repositoryApi.addCollaborator(
             githubOrg,
             name,
             username,
             "push",
           );

           const decryptedGithubAccessToken =
             this.decryptSecret(githubAccessToken);

           await userApi.acceptRepositoryInvitationByRepo(
             githubOrg,
             name,
             decryptedGithubAccessToken,
           );
         }),
       );
+
+      const failures = results.filter((r) => r.status === "rejected");
+      if (failures.length > 0) {
+        this.logger.warn(
+          `Failed to add ${failures.length} user(s) to repository ${name}`,
+        );
+      }
frontend/components/event-info-notice.tsx (1)

16-24: Stop the interval after the event starts.

Right now the interval continues to tick even when the event has started, causing a re-render every second for a banner that no longer renders. Consider stopping the interval once hasStarted flips true.

♻️ Proposed fix
-  useEffect(() => {
-    const id = setInterval(() => setNow(new Date()), 1000);
-    return () => clearInterval(id);
-  }, []);
+  useEffect(() => {
+    if (hasStarted) return;
+    const id = setInterval(() => setNow(new Date()), 1000);
+    return () => clearInterval(id);
+  }, [hasStarted]);

Comment on lines +35 to +47
const redirectUrl = this.configService.getOrThrow<string>(
"OAUTH_SUCCESS_REDIRECT_URL",
);
if (redirectUrl) {
res.cookie("token", token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 30 * 24 * 60 * 60 * 1000,
});
return res.redirect(redirectUrl);
}
return res.json({ token });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unnecessary conditional check after getOrThrow.

getOrThrow will throw an exception if the key is missing, so redirectUrl will always be truthy if execution reaches line 38. The if (redirectUrl) check is redundant and the else branch returning JSON is dead code.

🐛 Proposed fix

If you want to support both redirect and JSON response modes, use get() instead:

-    const redirectUrl = this.configService.getOrThrow<string>(
+    const redirectUrl = this.configService.get<string>(
       "OAUTH_SUCCESS_REDIRECT_URL",
     );

Or if redirect is always required, remove the conditional:

     const redirectUrl = this.configService.getOrThrow<string>(
       "OAUTH_SUCCESS_REDIRECT_URL",
     );
-    if (redirectUrl) {
       res.cookie("token", token, {
         httpOnly: true,
         secure: true,
         sameSite: "none",
         maxAge: 30 * 24 * 60 * 60 * 1000,
       });
       return res.redirect(redirectUrl);
-    }
-    return res.json({ token });
📝 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 redirectUrl = this.configService.getOrThrow<string>(
"OAUTH_SUCCESS_REDIRECT_URL",
);
if (redirectUrl) {
res.cookie("token", token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 30 * 24 * 60 * 60 * 1000,
});
return res.redirect(redirectUrl);
}
return res.json({ token });
const redirectUrl = this.configService.get<string>(
"OAUTH_SUCCESS_REDIRECT_URL",
);
if (redirectUrl) {
res.cookie("token", token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 30 * 24 * 60 * 60 * 1000,
});
return res.redirect(redirectUrl);
}
return res.json({ token });
Suggested change
const redirectUrl = this.configService.getOrThrow<string>(
"OAUTH_SUCCESS_REDIRECT_URL",
);
if (redirectUrl) {
res.cookie("token", token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 30 * 24 * 60 * 60 * 1000,
});
return res.redirect(redirectUrl);
}
return res.json({ token });
const redirectUrl = this.configService.getOrThrow<string>(
"OAUTH_SUCCESS_REDIRECT_URL",
);
res.cookie("token", token, {
httpOnly: true,
secure: true,
sameSite: "none",
maxAge: 30 * 24 * 60 * 60 * 1000,
});
return res.redirect(redirectUrl);
🤖 Prompt for AI Agents
In `@api/src/auth/auth.controller.ts` around lines 35 - 47, The code in
AuthController where redirectUrl is assigned via
this.configService.getOrThrow<string>("OAUTH_SUCCESS_REDIRECT_URL") is checking
if (redirectUrl) even though getOrThrow will always return or throw, making the
conditional dead code; either remove the if and always set the cookie +
res.redirect(redirectUrl) (if redirect is mandatory), or change getOrThrow to
this.configService.get<string>("OAUTH_SUCCESS_REDIRECT_URL") and keep the if to
allow fallback to return res.json({ token }) when redirectUrl is undefined —
update the block in auth.controller.ts (the redirectUrl variable and surrounding
cookie/redirect/JSON logic) accordingly.

Comment on lines +110 to +114
throw new BadRequestException(
e && typeof e.message === "string"
? `Invalid state parameter: ${e.message}`
: "Invalid state parameter.",
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid leaking internal error details to clients.

Including e.message in the response could expose internal implementation details (e.g., database errors, stack traces) to clients, which is a security concern.

🔒 Proposed fix
       throw new BadRequestException(
-        e && typeof e.message === "string"
-          ? `Invalid state parameter: ${e.message}`
-          : "Invalid state parameter.",
+        "Invalid state parameter.",
       );

The detailed error is already logged on line 106 for debugging purposes.

📝 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
throw new BadRequestException(
e && typeof e.message === "string"
? `Invalid state parameter: ${e.message}`
: "Invalid state parameter.",
);
throw new BadRequestException(
"Invalid state parameter.",
);
🤖 Prompt for AI Agents
In `@api/src/auth/auth.controller.ts` around lines 110 - 114, The thrown
BadRequestException in auth.controller.ts currently interpolates e.message and
may leak internal error details; change the throw to use a generic message
(e.g., "Invalid state parameter.") and remove inclusion of e.message while
leaving the existing debug log at the earlier line intact; target the
BadRequestException throw that references the variable e in the relevant method
of the AuthController to make this change.

Comment on lines +51 to +60
} else {
user = await this.users.createUser(
email || `${githubId}@users.noreply.github.com`,
username,
name,
profilePicture,
githubId,
accessToken,
false,
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle potentially undefined profilePicture for new users.

When creating a new user, profilePicture is passed directly from the profile which may be undefined. If the database column has a NOT NULL constraint or downstream code expects a string, this could cause issues.

🐛 Proposed fix
       user = await this.users.createUser(
         email || `${githubId}@users.noreply.github.com`,
         username,
         name,
-        profilePicture,
+        profilePicture || "",
         githubId,
         accessToken,
         false,
       );
🤖 Prompt for AI Agents
In `@api/src/auth/github.strategy.ts` around lines 51 - 60, The new-user creation
passes profilePicture from the GitHub profile which may be undefined; update the
call to users.createUser in github.strategy.ts (the block that constructs new
user via this.users.createUser) to provide a safe default (e.g., empty string or
a configured placeholder URL) using a nullish fallback for profilePicture so the
database always receives a string and downstream code won’t break.

Comment on lines 1 to 4
export const enum LockKeys {
AUTO_LOCK_EVENTS = 12345,
CREATE_TEAM_REPOS = 12346,
} No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for hardcoded lock key values that should use the LockKeys enum
rg -n "12345|12346" --type ts -g '!**/constants.ts'

Repository: 42core-team/website

Length of output: 123


Replace hardcoded lock key with LockKeys enum value in match.service.ts.

The LockKeys enum provides a centralized constant for lock key values, but match.service.ts (line 52) still uses the hardcoded value 12346 instead of LockKeys.CREATE_TEAM_REPOS. Update this line to use the enum for consistency and maintainability:

const lockKey = LockKeys.CREATE_TEAM_REPOS;
🤖 Prompt for AI Agents
In `@api/src/constants.ts` around lines 1 - 4, Replace the hardcoded lock key
literal 12346 in match.service.ts with the enum value
LockKeys.CREATE_TEAM_REPOS; locate where lockKey is assigned (the const lockKey
= ... usage) and change it to use LockKeys.CREATE_TEAM_REPOS, and ensure
LockKeys is imported from the constants module (add or update the import for
LockKeys if missing).

return;
}

const repoName = team.event.name + "-" + team.name + "-" + team.id;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sanitize repository name for GitHub compatibility.

GitHub repository names can only contain alphanumeric characters, hyphens, and underscores. If event.name or team.name contain spaces or special characters, the repository creation will fail.

🐛 Proposed fix
- const repoName = team.event.name + "-" + team.name + "-" + team.id;
+ const sanitize = (str: string) => str.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-");
+ const repoName = `${sanitize(team.event.name)}-${sanitize(team.name)}-${team.id}`;

Alternatively, move the sanitization logic to a utility function for reuse.

📝 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 repoName = team.event.name + "-" + team.name + "-" + team.id;
const sanitize = (str: string) => str.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-");
const repoName = `${sanitize(team.event.name)}-${sanitize(team.name)}-${team.id}`;
🤖 Prompt for AI Agents
In `@api/src/team/team.service.ts` at line 149, The repoName construction using
team.event.name + "-" + team.name + "-" + team.id must be sanitized for GitHub:
create (or call) a sanitizeRepoName utility and replace/normalize characters
from team.event.name and team.name (e.g., remove/replace non-alphanumeric
characters with hyphens or underscores, convert to lowercase, collapse
consecutive hyphens, trim leading/trailing hyphens, and enforce a sane length),
then build repoName = `${sanitize(part1)}-${sanitize(part2)}-${team.id}`; update
the code around the repoName assignment and export the sanitize function for
reuse.

Comment on lines 175 to 190
async createTeam(name: string, userId: string, eventId: string) {
return await this.dataSource.transaction(async (transaction) => {
const teamRepository = transaction.getRepository(TeamEntity);

const newTeam = await teamRepository.save({
name,
event: { id: eventId },
users: [{ id: userId }],
});

if (await this.eventService.hasEventStarted(eventId))
await this.createTeamRepository(newTeam.id);

return team;
return newTeam;
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential issue with nested independent transactions.

createTeam wraps its logic in a transaction, then calls createTeamRepository which starts its own independent transaction. If the outer transaction fails after createTeamRepository succeeds, the team record rolls back but the repo creation message may have been emitted. Consider either:

  1. Passing the transaction manager to createTeamRepository
  2. Moving the createTeamRepository call outside the transaction
🔒 Suggested approach: pass transaction manager
- async createTeamRepository(teamId: string) {
-   return await this.dataSource.transaction(async (transaction) => {
+ async createTeamRepository(teamId: string, transaction?: EntityManager) {
+   const execute = async (manager: EntityManager) => {
-     const teamRepository = transaction.getRepository(TeamEntity);
+     const teamRepository = manager.getRepository(TeamEntity);
      // ... rest of method
+   };
+   
+   if (transaction) {
+     return execute(transaction);
+   }
+   return this.dataSource.transaction(execute);
+ }

Then in createTeam:

      if (await this.eventService.hasEventStarted(eventId))
-       await this.createTeamRepository(newTeam.id);
+       await this.createTeamRepository(newTeam.id, transaction);
🤖 Prompt for AI Agents
In `@api/src/team/team.service.ts` around lines 175 - 190, The outer createTeam
wraps work in dataSource.transaction but then calls createTeamRepository which
starts its own independent transaction, risking inconsistent side-effects if the
outer transaction rolls back; update createTeamRepository to accept an optional
EntityManager (or TransactionalRepository) parameter and, inside createTeam,
pass the transaction manager (the value returned to the transaction callback)
when calling createTeamRepository (use eventService.hasEventStarted(eventId) as
before, then await createTeamRepository(newTeam.id, manager) so both DB
operations share the same transaction), or alternatively remove the
createTeamRepository call from inside the transaction and invoke it after the
transaction completes; adjust function signatures accordingly
(createTeamRepository(..., manager?: EntityManager)).

text: string;
variant: "default" | "secondary" | "destructive";
} => {
console.log("try to format state for event", event);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug console.log statement.

This debug statement will log for every event on every render, polluting the browser console in production.

🐛 Proposed fix
-    console.log("try to format state for event", event);
🤖 Prompt for AI Agents
In `@frontend/app/events/EventTable.tsx` at line 25, Remove the debug console.log
call that's printing every event in the EventTable component: delete the line
console.log("try to format state for event", event) (or replace it with a
development-only guard like if (process.env.NODE_ENV !== 'production') { /*
debug log */ }) so the EventTable rendering no longer spams the browser console;
locate the statement inside the EventTable component where the event is being
formatted and remove or conditionally guard it.

Comment on lines +35 to +45
if (event.currentRound === 0) {
return {
text: "In Progress",
variant: "secondary",
};
}

return {
text: "Completed",
variant: "destructive",
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: 42core-team/website

Length of output: 847


🏁 Script executed:

rg "currentRound" -C 3 --type ts --type tsx

Repository: 42core-team/website

Length of output: 90


🏁 Script executed:

rg "currentRound" -C 3 --type ts api/

Repository: 42core-team/website

Length of output: 12376


🏁 Script executed:

# Also check the event-related files directly
find api -name "*event*" -type f | head -20

Repository: 42core-team/website

Length of output: 467


🏁 Script executed:

cat -n frontend/app/events/EventTable.tsx | sed -n '25,55p'

Repository: 42core-team/website

Length of output: 956


🏁 Script executed:

# Also search for the function containing this logic
rg "currentRound === 0" -B 10 -A 5 frontend/app/events/EventTable.tsx

Repository: 42core-team/website

Length of output: 431


🏁 Script executed:

# Check for event state or completion indicators
rg "Completed|FINISHED" frontend/app/events/EventTable.tsx -B 3 -A 3

Repository: 42core-team/website

Length of output: 149


🏁 Script executed:

# Check the Event entity for other completion/state fields
cat -n api/src/event/entities/event.entity.ts

Repository: 42core-team/website

Length of output: 2908


Fix the event state logic to align with backend round semantics.

The frontend logic contradicts backend behavior. According to the backend, currentRound === 0 indicates that no rounds have started yet (see match.controller.ts and match.service.ts), yet the frontend displays this as "In Progress". Additionally, the logic assumes any currentRound > 0 means "Completed", but the backend only increments the round counter after a round finishes—the event could still be active if the current time hasn't reached endDate.

Update the state logic to:

  • Check currentRound === 0 to represent the initial phase (not "In Progress")
  • Check against endDate to determine if the event is actually completed
  • Properly represent in-progress rounds when currentRound > 0 and the event hasn't ended
🤖 Prompt for AI Agents
In `@frontend/app/events/EventTable.tsx` around lines 35 - 45, Replace the
existing status logic that checks only event.currentRound with logic that uses
both event.currentRound and event.endDate: treat event.currentRound === 0 as the
"Initial"/not-started state, treat the event as "Completed" only when new Date()
is after new Date(event.endDate), and treat event.currentRound > 0 with endDate
in the future as "In Progress"; update the return objects (text and variant)
accordingly where the current snippet lives (EventTable.tsx) using the
event.currentRound and event.endDate fields.

Comment on lines +19 to +32
const didRefreshRef = useRef(false);

useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);

useEffect(() => {
const timeLeftMs = startsAt.getTime() - now.getTime();
if (!didRefreshRef.current && timeLeftMs <= 0) {
didRefreshRef.current = true;
router.refresh();
}
}, [now, startsAt, router]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/components/event-info-notice.tsx

Repository: 42core-team/website

Length of output: 2619


Reset the refresh guard when startDate changes.

If this component remains mounted across navigation to a different event, didRefreshRef will retain its true state from the previous event, preventing the refresh when the new event's countdown reaches zero. Reset it when startDate changes.

Proposed fix
  const didRefreshRef = useRef(false);

+  useEffect(() => {
+    didRefreshRef.current = false;
+  }, [startDate]);
🤖 Prompt for AI Agents
In `@frontend/components/event-info-notice.tsx` around lines 19 - 32, The refresh
guard didRefreshRef is never reset when a new event (startsAt) is loaded, so add
logic to reset it whenever startsAt changes; specifically, add a small useEffect
that watches startsAt and sets didRefreshRef.current = false (referencing
didRefreshRef and startsAt) so the countdown can trigger router.refresh() for
the new event.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
api/src/match/match.service.ts (1)

139-140: Error message doesn't match the actual state being checked.

The code checks for MatchState.IN_PROGRESS but the error message says "READY state". This mismatch will cause confusion during debugging.

📝 Suggested fix
     if (match.state !== MatchState.IN_PROGRESS)
-      throw new Error(`Match with id ${matchId} is not in READY state.`);
+      throw new Error(`Match with id ${matchId} is not in IN_PROGRESS state.`);

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants