fix: resolve Prisma alunoId for ALUNO role to avoid FK violation#174
Conversation
getAuthRole() returns role=null for ALUNO (no funcionarios record). Previous code used auth user.id as alunoId, but Prisma aluno.id may differ from Supabase Auth UUID. Added resolveAlunoId() helper that looks up the correct aluno.id by email before create. Fixes: foreign key constraint violation on treinos_alunoId_fkey when ALUNO saves AI-generated workout.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughModifies ChangesAluno ID Resolution Fix
React table lint suppression
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/lib/actions/treinos.ts (1)
185-190: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winResolve
alunoIdonce instead of per-treino; drop the redundant spread override.For
role === null,resolveAlunoId(user)returns the same value for every treino in the batch, yet it runs one DB query per treino insidePromise.all(N redundant lookups). Also{ ...t, alunoId: t.alunoId }is a no-op sincealunoIdis already int. Resolve once before mapping.♻️ Suggested refactor
- const validated = await Promise.all( - treinos.map(async (t) => ({ - ...TreinoBaseSchema.parse({ ...t, alunoId: t.alunoId }), - alunoId: role === null ? await resolveAlunoId(user) : t.alunoId, - })) - ); + const resolvedAlunoId = role === null ? await resolveAlunoId(user) : null; + const validated = treinos.map((t) => { + const parsed = TreinoBaseSchema.parse(t); + return { ...parsed, alunoId: resolvedAlunoId ?? t.alunoId }; + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/actions/treinos.ts` around lines 185 - 190, Resolve `alunoId` once before the `Promise.all` map in `treinos.ts` instead of calling `resolveAlunoId(user)` for every item when `role === null`, and reuse that single value for each treino in the batch. Also remove the redundant `{ ...t, alunoId: t.alunoId }` spread/override in the `TreinoBaseSchema.parse` call, since `alunoId` already comes from `t`. Keep the change localized around the validation mapping logic in the `validated` निर्माण.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/actions/treinos.ts`:
- Around line 52-53: `resolveAlunoId` is still falling back to `user.id` for
ALUNO, which can reintroduce the `treinos_alunoId_fkey` violation; update this
helper to fail explicitly when the `prisma.aluno.findUnique` lookup does not
return a match for `role === null`. Also stop using `user.email ?? ''` in the
lookup, and handle a missing email as an error or invalid state rather than
querying with an empty string. Keep the fix centered in `resolveAlunoId` so the
caller only proceeds with a real `aluno.id`.
- Line 63: The ownership check in the treino flow is still comparing
existingTreino.alunoId against user.id, which can reject valid ALUNO owners when
their aluno id is different from the Supabase user id. Update the check in the
relevant treino action to compare against the resolved alunoId variable instead,
matching the create and batch paths, and use the existing
resolveAlunoId/validatedData.alunoId logic to keep ownership consistent.
---
Nitpick comments:
In `@src/lib/actions/treinos.ts`:
- Around line 185-190: Resolve `alunoId` once before the `Promise.all` map in
`treinos.ts` instead of calling `resolveAlunoId(user)` for every item when `role
=== null`, and reuse that single value for each treino in the batch. Also remove
the redundant `{ ...t, alunoId: t.alunoId }` spread/override in the
`TreinoBaseSchema.parse` call, since `alunoId` already comes from `t`. Keep the
change localized around the validation mapping logic in the `validated` निर्माण.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 13785a08-52b8-416f-ac32-aed44d5d88ab
📒 Files selected for processing (1)
src/lib/actions/treinos.ts
| const aluno = await prisma.aluno.findUnique({ where: { email: user.email ?? '' } }); | ||
| return aluno?.id ?? user.id; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Silent fallback reintroduces the FK violation this PR fixes.
For role === null (ALUNO), if the email lookup misses, resolveAlunoId returns user.id — the exact Supabase Auth UUID that causes treinos_alunoId_fkey to fail. Additionally, user.email ?? '' turns a missing email into a lookup for an aluno with an empty-string email, which can match the wrong record or silently miss. Consider failing explicitly for ALUNO when no matching aluno is found rather than falling back.
🛠️ Suggested handling
-async function resolveAlunoId(user: { id: string; email?: string | null }): Promise<string> {
- const aluno = await prisma.aluno.findUnique({ where: { email: user.email ?? '' } });
- return aluno?.id ?? user.id;
-}
+async function resolveAlunoId(user: { id: string; email?: string | null }): Promise<string> {
+ if (!user.email) throw new Error('E-mail do usuário não disponível para resolver o aluno.');
+ const aluno = await prisma.aluno.findUnique({ where: { email: user.email } });
+ if (!aluno) throw new Error('Aluno correspondente não encontrado.');
+ return aluno.id;
+}📝 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.
| const aluno = await prisma.aluno.findUnique({ where: { email: user.email ?? '' } }); | |
| return aluno?.id ?? user.id; | |
| if (!user.email) throw new Error('E-mail do usuário não disponível para resolver o aluno.'); | |
| const aluno = await prisma.aluno.findUnique({ where: { email: user.email } }); | |
| if (!aluno) throw new Error('Aluno correspondente não encontrado.'); | |
| return aluno.id; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/actions/treinos.ts` around lines 52 - 53, `resolveAlunoId` is still
falling back to `user.id` for ALUNO, which can reintroduce the
`treinos_alunoId_fkey` violation; update this helper to fail explicitly when the
`prisma.aluno.findUnique` lookup does not return a match for `role === null`.
Also stop using `user.email ?? ''` in the lookup, and handle a missing email as
an error or invalid state rather than querying with an empty string. Keep the
fix centered in `resolveAlunoId` so the caller only proceeds with a real
`aluno.id`.
| ) { | ||
| const { objetivo, exercicios, diaSemana } = validatedData; | ||
| const alunoId = role === null ? user.id : validatedData.alunoId; | ||
| const alunoId = role === null ? await resolveAlunoId(user) : validatedData.alunoId; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '1,180p' src/lib/actions/treinos.tsRepository: EmiyaKiritsugu3/PWeb_Project
Length of output: 6322
🏁 Script executed:
rg -n "resolveAlunoId|existingTreino\.alunoId === user\.id|aluno\.id may differ|alunoId === user\.id" src/lib/actions src/lib utilsRepository: EmiyaKiritsugu3/PWeb_Project
Length of output: 1052
Use the resolved alunoId in the ownership check
existingTreino.alunoId === user.id will block ALUNO owners when aluno.id differs from the Supabase user id. Compare against the resolved alunoId here, matching the create/batch paths.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/actions/treinos.ts` at line 63, The ownership check in the treino
flow is still comparing existingTreino.alunoId against user.id, which can reject
valid ALUNO owners when their aluno id is different from the Supabase user id.
Update the check in the relevant treino action to compare against the resolved
alunoId variable instead, matching the create and batch paths, and use the
existing resolveAlunoId/validatedData.alunoId logic to keep ownership
consistent.
There was a problem hiding this comment.
4 issues found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/lib/actions/treinos.ts">
<violation number="1" location="src/lib/actions/treinos.ts:51">
P2: Custom agent: **Enforce Pragmatic Test Coverage**
New `resolveAlunoId` logic is core business logic with two distinct paths (aluno found by email / fallback to auth id), but neither path is tested. The existing `treinos.test.ts` covers `upsertTreinoAction` and `batchUpsertTreinoAction` for INSTRUTOR, GERENTE, and RECEPCIONISTA roles, yet there are no test cases for the ALUNO role (`role === null`) that would exercise `resolveAlunoId`. This leaves the success and fallback paths uncovered.</violation>
<violation number="2" location="src/lib/actions/treinos.ts:52">
P1: Custom agent: **Enforce Strict Maintainability Standards**
The `resolveAlunoId` helper silently falls back to `user.id` when no Prisma `aluno` record is found by email. Because the PR itself notes that `aluno.id` may differ from the Auth `user.id`, this fallback can still cause the FK violation the PR is fixing and masks a data-integrity issue. The same file already handles a missing aluno explicitly in `registrarHistoricoTreinoAction` (line 356) by throwing an error; `resolveAlunoId` should follow that pattern instead of using a silent fallback.</violation>
<violation number="3" location="src/lib/actions/treinos.ts:63">
P0: ALUNO users will be unable to edit their own treinos after this change. The authorization check `existingTreino.alunoId === user.id` at line 75 compares the stored Prisma `alunoId` against the auth `user.id`. Before this fix, both were the same (auth user ID). Now the CREATE flow stores the resolved Prisma aluno ID, but the UPDATE authorization check still uses `user.id`. When a student creates a treino and later tries to edit it, the stored `alunoId` is the Prisma ID, which differs from the auth `user.id`, so the check fails returning "Acesso não autorizado para edição". The fix should also check against the resolved `alunoId`, e.g., `existingTreino.alunoId === user.id || existingTreino.alunoId === alunoId`.</violation>
<violation number="4" location="src/lib/actions/treinos.ts:186">
P2: In the batch upsert flow, `resolveAlunoId(user)` is called once per treino inside a `Promise.all(treinos.map(...))`, but the result is always the same for the same user. This generates N identical `prisma.aluno.findUnique` queries instead of one. For a batch of 10 treinos, that's 9 unnecessary database round-trips in parallel. Consider hoisting the call before the loop: `const resolvedAlunoId = role === null ? await resolveAlunoId(user) : undefined;` then reference `resolvedAlunoId` inside the map.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| ) { | ||
| const { objetivo, exercicios, diaSemana } = validatedData; | ||
| const alunoId = role === null ? user.id : validatedData.alunoId; | ||
| const alunoId = role === null ? await resolveAlunoId(user) : validatedData.alunoId; |
There was a problem hiding this comment.
P0: ALUNO users will be unable to edit their own treinos after this change. The authorization check existingTreino.alunoId === user.id at line 75 compares the stored Prisma alunoId against the auth user.id. Before this fix, both were the same (auth user ID). Now the CREATE flow stores the resolved Prisma aluno ID, but the UPDATE authorization check still uses user.id. When a student creates a treino and later tries to edit it, the stored alunoId is the Prisma ID, which differs from the auth user.id, so the check fails returning "Acesso não autorizado para edição". The fix should also check against the resolved alunoId, e.g., existingTreino.alunoId === user.id || existingTreino.alunoId === alunoId.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/actions/treinos.ts, line 63:
<comment>ALUNO users will be unable to edit their own treinos after this change. The authorization check `existingTreino.alunoId === user.id` at line 75 compares the stored Prisma `alunoId` against the auth `user.id`. Before this fix, both were the same (auth user ID). Now the CREATE flow stores the resolved Prisma aluno ID, but the UPDATE authorization check still uses `user.id`. When a student creates a treino and later tries to edit it, the stored `alunoId` is the Prisma ID, which differs from the auth `user.id`, so the check fails returning "Acesso não autorizado para edição". The fix should also check against the resolved `alunoId`, e.g., `existingTreino.alunoId === user.id || existingTreino.alunoId === alunoId`.</comment>
<file context>
@@ -43,14 +43,24 @@ async function getAuthRole() {
) {
const { objetivo, exercicios, diaSemana } = validatedData;
- const alunoId = role === null ? user.id : validatedData.alunoId;
+ const alunoId = role === null ? await resolveAlunoId(user) : validatedData.alunoId;
const id = 'id' in validatedData ? (validatedData as TreinoBase & { id: string }).id : undefined;
</file context>
| * Resolve the correct alunoId from Prisma by email to avoid FK violation. | ||
| */ | ||
| async function resolveAlunoId(user: { id: string; email?: string | null }): Promise<string> { | ||
| const aluno = await prisma.aluno.findUnique({ where: { email: user.email ?? '' } }); |
There was a problem hiding this comment.
P1: Custom agent: Enforce Strict Maintainability Standards
The resolveAlunoId helper silently falls back to user.id when no Prisma aluno record is found by email. Because the PR itself notes that aluno.id may differ from the Auth user.id, this fallback can still cause the FK violation the PR is fixing and masks a data-integrity issue. The same file already handles a missing aluno explicitly in registrarHistoricoTreinoAction (line 356) by throwing an error; resolveAlunoId should follow that pattern instead of using a silent fallback.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/actions/treinos.ts, line 52:
<comment>The `resolveAlunoId` helper silently falls back to `user.id` when no Prisma `aluno` record is found by email. Because the PR itself notes that `aluno.id` may differ from the Auth `user.id`, this fallback can still cause the FK violation the PR is fixing and masks a data-integrity issue. The same file already handles a missing aluno explicitly in `registrarHistoricoTreinoAction` (line 356) by throwing an error; `resolveAlunoId` should follow that pattern instead of using a silent fallback.</comment>
<file context>
@@ -43,14 +43,24 @@ async function getAuthRole() {
+ * Resolve the correct alunoId from Prisma by email to avoid FK violation.
+ */
+async function resolveAlunoId(user: { id: string; email?: string | null }): Promise<string> {
+ const aluno = await prisma.aluno.findUnique({ where: { email: user.email ?? '' } });
+ return aluno?.id ?? user.id;
+}
</file context>
| * Their Prisma `aluno.id` may differ from Supabase Auth `user.id`. | ||
| * Resolve the correct alunoId from Prisma by email to avoid FK violation. | ||
| */ | ||
| async function resolveAlunoId(user: { id: string; email?: string | null }): Promise<string> { |
There was a problem hiding this comment.
P2: Custom agent: Enforce Pragmatic Test Coverage
New resolveAlunoId logic is core business logic with two distinct paths (aluno found by email / fallback to auth id), but neither path is tested. The existing treinos.test.ts covers upsertTreinoAction and batchUpsertTreinoAction for INSTRUTOR, GERENTE, and RECEPCIONISTA roles, yet there are no test cases for the ALUNO role (role === null) that would exercise resolveAlunoId. This leaves the success and fallback paths uncovered.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/actions/treinos.ts, line 51:
<comment>New `resolveAlunoId` logic is core business logic with two distinct paths (aluno found by email / fallback to auth id), but neither path is tested. The existing `treinos.test.ts` covers `upsertTreinoAction` and `batchUpsertTreinoAction` for INSTRUTOR, GERENTE, and RECEPCIONISTA roles, yet there are no test cases for the ALUNO role (`role === null`) that would exercise `resolveAlunoId`. This leaves the success and fallback paths uncovered.</comment>
<file context>
@@ -43,14 +43,24 @@ async function getAuthRole() {
+ * Their Prisma `aluno.id` may differ from Supabase Auth `user.id`.
+ * Resolve the correct alunoId from Prisma by email to avoid FK violation.
+ */
+async function resolveAlunoId(user: { id: string; email?: string | null }): Promise<string> {
+ const aluno = await prisma.aluno.findUnique({ where: { email: user.email ?? '' } });
+ return aluno?.id ?? user.id;
</file context>
|
|
||
| const validated = treinos.map((t) => TreinoBaseSchema.parse({ ...t, alunoId: t.alunoId })); | ||
| const validated = await Promise.all( | ||
| treinos.map(async (t) => ({ |
There was a problem hiding this comment.
P2: In the batch upsert flow, resolveAlunoId(user) is called once per treino inside a Promise.all(treinos.map(...)), but the result is always the same for the same user. This generates N identical prisma.aluno.findUnique queries instead of one. For a batch of 10 treinos, that's 9 unnecessary database round-trips in parallel. Consider hoisting the call before the loop: const resolvedAlunoId = role === null ? await resolveAlunoId(user) : undefined; then reference resolvedAlunoId inside the map.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/actions/treinos.ts, line 186:
<comment>In the batch upsert flow, `resolveAlunoId(user)` is called once per treino inside a `Promise.all(treinos.map(...))`, but the result is always the same for the same user. This generates N identical `prisma.aluno.findUnique` queries instead of one. For a batch of 10 treinos, that's 9 unnecessary database round-trips in parallel. Consider hoisting the call before the loop: `const resolvedAlunoId = role === null ? await resolveAlunoId(user) : undefined;` then reference `resolvedAlunoId` inside the map.</comment>
<file context>
@@ -172,15 +182,20 @@ export async function batchUpsertTreinoAction(
- const validated = treinos.map((t) => TreinoBaseSchema.parse({ ...t, alunoId: t.alunoId }));
+ const validated = await Promise.all(
+ treinos.map(async (t) => ({
+ ...TreinoBaseSchema.parse({ ...t, alunoId: t.alunoId }),
+ alunoId: role === null ? await resolveAlunoId(user) : t.alunoId,
</file context>
…n TanStack Table useReactTable CI Quality Gates failing on data-table.tsx:46 — react-hooks/incompatible-library warning treated as failure. TanStack Table useReactTable returns non-memoizable fns by design; React Compiler rule is a false positive. Suppression unblocks PR #174 + main CI. Co-Authored-By: Claude <noreply@anthropic.com>
…ier 3.9.4 data-table.tsx: eslint-disable useReactTable incompatible-library rule (TanStack fns non-memoizable by design, false positive). middleware.ts: prettier 3.9.4 reformats union type (local 3.8.3 stale vs CI 3.9.4). Unblocks PR #174 Quality Gates and broken main CI. Co-Authored-By: Claude <noreply@anthropic.com>
Auto-generated plan files not source code. Broke CI format gate on main + PR #174. Co-Authored-By: Claude <noreply@anthropic.com>
|
…rization deleteTreinoAction and updateTreinoDayAction compared treino.alunoId (Prisma ID) against user.id (Auth UUID), always mismatching after PR #174 FK fix. Resolve aluno.id by email before auth check. Co-Authored-By: Claude <noreply@anthropic.com>
…nos (#175) * docs: meus treinos UX improvements design spec Co-Authored-By: Claude <noreply@anthropic.com> * docs: detailed implementation plan for meus treinos UX improvements Co-Authored-By: Claude <noreply@anthropic.com> * feat(workout-editor): add compact prop to drop outer Card wrapper - Add compact?: boolean prop (default false) to WorkoutEditor - Extract JSX into const content, conditionally wrap in div vs Card - Add 2 tests: compact omits Card, default renders Card Co-Authored-By: Claude <noreply@anthropic.com> * feat(ux): expose planName from useWorkoutGeneration for visual grouper banner * feat(ux): inline editor inside treino cards, remove bottom editor - editingTreinoId state drives inline WorkoutEditor per card - handleEditLocal replaces scrollTo behavior - Criar Manual button moved above list, opens inline editor - Removed bottom-of-page conditional editor - Removed window.scrollTo from use-workout-crud Co-Authored-By: Claude <noreply@anthropic.com> * fix(test): update use-workout-crud tests for inline editor changes Co-Authored-By: Claude <noreply@anthropic.com> * feat(ux): visual grouper banner + after-gen scroll anchor Co-Authored-By: Claude <noreply@anthropic.com> * test(ux): update meus-treinos-client tests for planName banner Co-Authored-By: Claude <noreply@anthropic.com> * fix(ux): reset planName before each generation Ensures banner re-triggers on every AI gen, not just first time. Co-Authored-By: Claude <noreply@anthropic.com> * fix(ux): add plan description to banner card Co-Authored-By: Claude <noreply@anthropic.com> * fix(ux): retry scroll with requestAnimationFrame until DOM ready Replaces fixed 300ms setTimeout with rAF loop that retries until #treinos-pessoais anchor exists in DOM. Eliminates race condition on slow devices / slow RSC renders. Co-Authored-By: Claude <noreply@anthropic.com> * fix(auth): resolve Prisma alunoId vs Auth UUID in delete+update authorization deleteTreinoAction and updateTreinoDayAction compared treino.alunoId (Prisma ID) against user.id (Auth UUID), always mismatching after PR #174 FK fix. Resolve aluno.id by email before auth check. Co-Authored-By: Claude <noreply@anthropic.com> * fix(review): address PR #175 review findings — all severity levels - rAF scroll capped at 20 attempts (prevents infinite loop) - handleDelete + handleDayChange: surface server errors to user - replace inline prisma.aluno.findUnique with resolveAlunoId() for auth - add onSuccess to useCallback dep array Co-Authored-By: Claude <noreply@anthropic.com> * fix(type): make onSuccess optional in useWorkoutGeneration Pre-existing tests don't pass onSuccess — made optional to avoid 12 TS2345 errors. Caller (meus-treinos-client) always passes it. Co-Authored-By: Claude <noreply@anthropic.com> * fix(ux): keep editor open on save failure + dead code cleanup handleSave returns boolean. Client closes editor only on success. Removed unused isFormVisible, editingTreino, handleEdit from hook. Kept __new__ sentinel for now. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Emiya Kiritsugu <emiyakiritsugu3@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>



Problema
FK violation
treinos_alunoId_fkeyao salvar treino gerado por IA.Causa
performTreinoUpsertusavauser.id(Auth UUID) comoalunoIdp/ ALUNO.Registro
alunono Prisma pode ter UUID diferente do Auth.Fix
Nova
resolveAlunoId()buscaaluno.idno Prisma pelo email antes do create.Aplicada em
performTreinoUpsertebatchUpsertTreinoAction.Verificação
npm run typecheck: 0 errorsnpm run test: all passSummary by cubic
Fixes FK violations when ALUNO users save treinos by resolving the correct Prisma alunoId from email. Also unblocks CI by suppressing a false-positive lint, aligning
middleware.tsformatting withprettier3.9.4, and excludingdocs/superpowers/from Prettier checks.aluno.idby email for ALUNO role and use it in single and batch upserts; preventstreinos_alunoId_fkeyerrors.react-hooks/incompatible-libraryfalse positive for TanStackuseReactTableand reformatmiddleware.tsunion type to matchprettier3.9.4.docs/superpowers/fromprettierto prevent format gate failures on auto-generated files.Written for commit 004e4af. Summary will update on new commits.
Summary by CodeRabbit