Skip to content

Commit 3312af6

Browse files
feat(it5): INSTRUTOR authorization hardening (#7) (#81)
* docs(it5): open It5-T01 branch — update CURRENT-STATE + CLAUDE.md Marks branch feat/007-it5-instrutor-auth as active. CURRENT-STATE.md reflects the 12-task T01 plan (INSTRUTOR auth hardening), promotes the auth gap to P1 in the incomplete table, and extends the auth.ts key files entry. CLAUDE.md updated with It5 tech context via agent script. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(it5): document pre-existing TS2882 regression in quality gates globals.css side-effect import errors since src/types/css.d.ts was deleted in PR #80 (TypeScript 6 upgrade). Tracked as P2 — must be fixed before any It5 PR merges to main. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(typecheck): restore src/types/css.d.ts deleted in PR #80 TS6 upgrade TS2882 side-effect import error on globals.css was introduced when the TS6 upgrade PR accidentally removed this ambient declaration file. One line: declare module '*.css' {} Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(it5): mark TS2882 resolved in CURRENT-STATE quality gates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): add requireAnyRole helper to auth.ts T001 — exports requireAnyRole(allowedRoles: Role[]): Promise<void> following the exact same fail-closed pattern as requireRole. redirects to /login if unauthenticated, /dashboard if role is not in allowedRoles or on any DB error. needed by the /dashboard/treinos route gate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): add failing E2E negative auth test (TDD red) T002 — instrutor-auth-negative.spec.ts asserts RECEPCIONISTA and ALUNO cannot access /dashboard/treinos (must redirect). tests fail until T003 adds the requireAnyRole gate to the page — that is the intended red phase. also anchors /specs/ in .gitignore to root so tests/e2e/specs/ is no longer inadvertently excluded. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): gate /dashboard/treinos with requireAnyRole T003 — replaces bare if(!user) check with requireAnyRole(['INSTRUTOR', 'GERENTE']). RECEPCIONISTA and ALUNO are now redirected fail-closed to /dashboard. getUser() call kept temporarily for instrutorId={user.id} prop; removed in T008 after client cleanup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): add failing unit tests for treinos actions (TDD red) T004 + T009 — treinos.test.ts covers upsertTreinoAction (instrutorId derivation) and ownership checks for deleteTreinoAction and updateTreinoDayAction. 4 tests currently fail (red phase): INSTRUTOR instrutorId derivation, RECEPCIONISTA blocked, and both non-owner ownership guards. tests written before implementation as required by constitution principle III. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): remove instrutorId from TreinoBaseSchema (server-derived) T005: instrutorId is now server-derived from session in upsertTreinoAction. Removed from TreinoBaseSchema (client input); preserved in TreinoSchema (entity read-type). Cascade errors in treinos.ts:35 and treinos-client.tsx:384,433 are intentional red state — fixed in T006/T007. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): derive instrutorId from session in upsertTreinoAction T006: instrutorId is now fetched from the funcionarios table instead of the client payload. RECEPCIONISTA is blocked (Acesso não autorizado). ALUNO (not in funcionarios → data=null) and GERENTE both receive instrutorId=null. INSTRUTOR receives their own user.id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): remove instrutorId prop from TreinosManagementClient T007: instrutorId is no longer needed as a component prop since upsertTreinoAction now derives it server-side from session. Removed from props interface and both upsertTreinoAction call sites. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): remove redundant getUser and instrutorId prop from TreinosPage T008: requireAnyRole already asserts authentication, so the subsequent supabase.auth.getUser() call and instrutorId={user.id} prop are removed. instrutorId is now fully derived server-side in upsertTreinoAction. Zero typecheck errors after this task. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): add ownership check to updateTreinoDayAction T010: INSTRUTOR can only update treinos they own (instrutorId = user.id). GERENTE can update any treino. Unauthorized callers receive { success: false, error: 'Acesso não autorizado' }. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(it5): add ownership check to deleteTreinoAction T011: mirrors T010 pattern — INSTRUTOR can only delete treinos they own; GERENTE can delete any treino. All 10 unit tests green. Zero typecheck and lint errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(it5): update CRITICAL-PATHS.md 18 → 19 scenarios T012: added entry for instrutor-auth-negative.spec.ts (RECEPCIONISTA/ALUNO blocked from /dashboard/treinos). All 12 tasks for 007-it5-instrutor-auth are now complete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update CURRENT-STATE.md for It5 completion All 12 tasks complete. Auth gap closed. Unit tests 22→32. E2E scenarios 18→19. Auth gap removed from incomplete list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(it5): add requireAnyRole unit tests to restore auth.ts 100% coverage CI coverage threshold for src/lib/auth.ts requires 100%. Adding requireAnyRole tests mirrors the requireRole test structure: allowed role resolves without redirect, wrong role → /dashboard, no user → /login, DB error → /dashboard, null record (ALUNO) → /dashboard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(auth): address CodeRabbit review findings on PR #81 auth.ts: requireRole now delegates to requireAnyRole([role]) eliminating duplicated auth/redirect logic; consistent fail-closed behavior guaranteed by single implementation. treinos.ts: fix two security issues flagged by CodeRabbit: - upsertTreinoAction: ALUNOs (funcData=null) now get alunoId=user.id server-side, preventing cross-user spoofing of alunoId from client - updateTreinoDayAction + deleteTreinoAction: ownership guard now also checks treino.alunoId === user.id, allowing ALUNOs to manage their own treinos (previously blocked because instrutorId is null on ALUNO-created treinos) instrutor-auth-negative.spec.ts: ALUNO redirect test now asserts destination /aluno (middleware redirects non-funcionarios to /aluno) instead of weak negative assertion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Emiya Kiritsugu <emiyakiritsugu3@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b96e418 commit 3312af6

13 files changed

Lines changed: 465 additions & 93 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ walkthrough.md
109109
.remember/
110110
.specify/
111111

112-
# Feature planning artifacts
113-
specs/
112+
# Feature planning artifacts (root-level only — not tests/e2e/specs/)
113+
/specs/
114114

115115
# Dev scripts (not part of the build)
116116
scripts/

CLAUDE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# PWeb_Project Development Guidelines
22

3-
Auto-generated from all feature plans. Last updated: 2026-04-19
3+
Auto-generated from all feature plans. Last updated: 2026-04-21
44

55
## Active Technologies
66

7+
- TypeScript 5 (strict mode, `useUnknownInCatchVariables`) + Next.js 15 App Router, Supabase Auth SSR (`@supabase/ssr`), Prisma 7, Zod 3 (feat/007-it5-instrutor-auth)
8+
- PostgreSQL via Prisma (no migration for this spec) (feat/007-it5-instrutor-auth)
9+
710
- TypeScript 5 (strict mode) + Playwright 1.59, Next.js 15 App Router, Supabase local CLI (feat/006-it4-instrutor-e2e)
811
- PostgreSQL via Prisma (local Supabase, ports 54321/54322) (feat/006-it4-instrutor-e2e)
912

@@ -42,10 +45,11 @@ TypeScript 5 (strict mode, `useUnknownInCatchVariables` enabled): Follow standar
4245

4346
## Recent Changes
4447

48+
- feat/007-it5-instrutor-auth: Added TypeScript 5 (strict mode, `useUnknownInCatchVariables`) + Next.js 15 App Router, Supabase Auth SSR (`@supabase/ssr`), Prisma 7, Zod 3
49+
4550
- feat/006-it4-instrutor-e2e: Added TypeScript 5 (strict mode) + Playwright 1.59, Next.js 15 App Router, Supabase local CLI
4651

4752
- 004-elite-workflow-setup: Added TypeScript 5 (strict mode, `useUnknownInCatchVariables`) + Node.js 20 + Next.js 15 App Router, Prisma 7, Supabase SSR, Zod 3, Genkit 1.31
48-
- fix/telemetry-and-e2e-stability: Modernized Sentry to v10 + Next.js 15, implemented Privacy-First Replay, linked Supabase User IDs to observability, and repaired ESLint flat config.
4953

5054
<!-- MANUAL ADDITIONS START -->
5155

docs/CURRENT-STATE.md

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,55 @@
11
# Current State — Five Star Academy
22

3-
**Last Updated**: 2026-04-20
4-
**Branch**: `main` (v0.7.0It4 merged PR #73)
5-
**Version**: 0.7.0 (It4 completeINSTRUTOR workflow E2E, 18/18 passing)
3+
**Last Updated**: 2026-04-21
4+
**Branch**: `feat/007-it5-instrutor-auth` (It5all 12 tasks complete, PR ready)
5+
**Version**: 0.7.0 (It4 baseIt5 hardening pending merge)
66

77
## What Works Today
88

9-
| Feature | Status | Notes |
10-
| ------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------- |
11-
| Admin login | ✅ Working | Supabase Auth SSR |
12-
| Student login (Portal do Aluno) | ✅ Working | Separate session |
13-
| Admin dashboard | ✅ Working | GERENTE + RECEPCIONISTA |
14-
| Financial routes (`/financeiro`, `/planos`) | ✅ Working | GERENTE-only gate |
15-
| Student workout view | ✅ Working | `meus-treinos` |
16-
| AI workout generator | ✅ Working | Genkit + Gemini |
17-
| Student enrollment | ✅ Working | Admin creates aluno |
18-
| Gamification (XP, streaks) | ✅ Working | Hook `use-workout-tracker` |
19-
| Prisma migrations | ✅ Tracked | `prisma/migrations/` |
20-
| ESLint quality gate | ✅ Done | 0 errors — `any` + unused vars |
21-
| TypeScript typecheck | ✅ Clean | 0 errors (strict mode) |
22-
| AI workout feedback (US06) | ✅ Done | `WorkoutSession.tsx` — Genkit call + try/catch fallback + feedback card |
23-
| Unit tests | ✅ Passing | 22/22 (Vitest) |
24-
| Ops documentation | ✅ Done | Runbook, SLOs, threat model |
25-
| Process documentation | ✅ Done | RFC + Postmortem templates |
26-
| Local E2E stack | ✅ Done | `supabase start` (Docker) |
27-
| E2E seed script | ✅ Done | `prisma/seed-e2e.ts` (5 fixtures: 4 users + 1 treino with 2 exercícios) |
28-
| Playwright E2E suite | ✅ Done | 18 scenarios (+ instrutor-workflow: INSTRUTOR assigns workout → ALUNO sees "Do Personal" badge) |
29-
| CI E2E job | ✅ Green | 18/18 passing locally (It4); 17/17 in last CI (PR #72); PR pending |
30-
| Sentry error tracking | ✅ Active | DSN + auth token set in Vercel; source maps (92) uploaded on every build |
31-
| Structured logging | ✅ Done | `src/lib/logger.ts` (Logger wrapper, Sentry-aware) |
32-
| Dependencies | ✅ Updated | All patch/minor bumped via PR #70; lockfile clean |
9+
| Feature | Status | Notes |
10+
| ------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------- |
11+
| Admin login | ✅ Working | Supabase Auth SSR |
12+
| Student login (Portal do Aluno) | ✅ Working | Separate session |
13+
| Admin dashboard | ✅ Working | GERENTE + RECEPCIONISTA |
14+
| Financial routes (`/financeiro`, `/planos`) | ✅ Working | GERENTE-only gate |
15+
| Student workout view | ✅ Working | `meus-treinos` |
16+
| AI workout generator | ✅ Working | Genkit + Gemini |
17+
| Student enrollment | ✅ Working | Admin creates aluno |
18+
| Gamification (XP, streaks) | ✅ Working | Hook `use-workout-tracker` |
19+
| Prisma migrations | ✅ Tracked | `prisma/migrations/` |
20+
| ESLint quality gate | ✅ Done | 0 errors — `any` + unused vars |
21+
| TypeScript typecheck | ✅ Clean | 0 errors (strict mode) |
22+
| AI workout feedback (US06) | ✅ Done | `WorkoutSession.tsx` — Genkit call + try/catch fallback + feedback card |
23+
| Unit tests | ✅ Passing | 32/32 (Vitest — +10 for It5 treinos auth tests) |
24+
| Ops documentation | ✅ Done | Runbook, SLOs, threat model |
25+
| Process documentation | ✅ Done | RFC + Postmortem templates |
26+
| Local E2E stack | ✅ Done | `supabase start` (Docker) |
27+
| E2E seed script | ✅ Done | `prisma/seed-e2e.ts` (5 fixtures: 4 users + 1 treino with 2 exercícios) |
28+
| Playwright E2E suite | ✅ Done | 19 scenarios (18 It4 + instrutor-auth-negative: RECEPCIONISTA/ALUNO blocked from /dashboard/treinos) |
29+
| CI E2E job | ✅ Green | 18/18 passing locally (It4); 17/17 in last CI (PR #72); PR pending |
30+
| Sentry error tracking | ✅ Active | DSN + auth token set in Vercel; source maps (92) uploaded on every build |
31+
| Structured logging | ✅ Done | `src/lib/logger.ts` (Logger wrapper, Sentry-aware) |
32+
| Dependencies | ✅ Updated | All patch/minor bumped via PR #70; lockfile clean |
33+
34+
## It5 Progress (007-it5-instrutor-auth)
35+
36+
**Spec**: `specs/007-it5-instrutor-auth/` — all Phase 0–1 artifacts complete (spec, plan, research, data-model, quickstart, tasks)
37+
**12 tasks — ALL COMPLETE**
38+
39+
| Task | Description | Status |
40+
| ---- | ------------------------------------------------------------- | ----------- |
41+
| T001 | Add `requireAnyRole` to `src/lib/auth.ts` | ✅ Complete |
42+
| T002 | Create E2E negative test (RECEPCIONISTA/ALUNO blocked) | ✅ Complete |
43+
| T003 | Gate `/dashboard/treinos` with `requireAnyRole` | ✅ Complete |
44+
| T004 | Write unit tests for `upsertTreinoAction` (TDD red) | ✅ Complete |
45+
| T005 | Remove `instrutorId` from `TreinoBaseSchema` | ✅ Complete |
46+
| T006 | Harden `upsertTreinoAction` (derive instrutorId from session) | ✅ Complete |
47+
| T007 | Remove `instrutorId` prop from `TreinosManagementClient` | ✅ Complete |
48+
| T008 | Remove `getUser` + `instrutorId` prop from `TreinosPage` | ✅ Complete |
49+
| T009 | Write ownership unit tests (TDD red) | ✅ Complete |
50+
| T010 | Add ownership check to `updateTreinoDayAction` | ✅ Complete |
51+
| T011 | Add ownership check to `deleteTreinoAction` | ✅ Complete |
52+
| T012 | Update `CRITICAL-PATHS.md` 18 → 19 scenarios | ✅ Complete |
3353

3454
## What Is Incomplete
3555

@@ -42,10 +62,10 @@
4262
## Quality Gates (current status)
4363

4464
```
45-
npm run typecheck → ✅ 0 errors
65+
npm run typecheck → ✅ 0 errors (TS2882 fixed: src/types/css.d.ts restored)
4666
npm run lint → ✅ 0 errors
47-
npm run test → ✅ 22/22 passing
48-
npm run e2e → ✅ 18/18 passing (local, 2026-04-19 — It4)
67+
npm run test → ✅ 32/32 passing (+10 It5 auth tests)
68+
npm run e2e → ⏳ 19 scenarios (18 confirmed It4; #19 needs E2E run against local stack)
4969
npm run build → ✅ production build succeeds
5070
```
5171

@@ -58,7 +78,7 @@ npm run build → ✅ production build succeeds
5878
- **Validation**: Zod 3
5979
- **AI**: Google Genkit 1.32 + Gemini 2.5 Flash
6080
- **Styling**: Tailwind CSS 4 + Shadcn/UI
61-
- **Testing**: Vitest 4 (5 files, 22 unit tests) + Playwright 1.59 (18 E2E scenarios)
81+
- **Testing**: Vitest 4 (6 files, 32 unit tests) + Playwright 1.59 (19 E2E scenarios)
6282
- **CI**: GitHub Actions (quality + test + e2e jobs)
6383
- **Local E2E DB**: Supabase CLI (`supabase start` → ports 54321/54322)
6484

@@ -69,7 +89,7 @@ npm run build → ✅ production build succeeds
6989
| `src/app/dashboard/` | Admin portal pages |
7090
| `src/app/aluno/` | Student portal pages |
7191
| `src/lib/actions/` | Server Actions (auth, alunos, treinos, financeiro) |
72-
| `src/lib/auth.ts` | `requireRole()` helper |
92+
| `src/lib/auth.ts` | `requireRole()` + `requireAnyRole()` (It5 — live) |
7393
| `src/services/` | Business logic (XP, streaks) |
7494
| `src/ai/flows/` | Genkit AI flows |
7595
| `prisma/schema.prisma` | DB schema |

src/app/aluno/meus-treinos/meus-treinos-client.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,8 @@ export default function MeusTreinosClient({
7373
try {
7474
const res = await upsertTreinoAction({
7575
...treinoData,
76-
id: editingTreino?.id,
76+
...(editingTreino ? { id: editingTreino.id } : {}),
7777
alunoId: userId,
78-
instrutorId: userId,
7978
});
8079

8180
if (res.success) {
@@ -192,7 +191,6 @@ export default function MeusTreinosClient({
192191

193192
const res = await upsertTreinoAction({
194193
alunoId: userId,
195-
instrutorId: undefined,
196194
objetivo: workout.nome,
197195
exercicios: novosExercicios,
198196
diaSemana: isDayOccupied ? null : diaSugerido,

src/app/dashboard/treinos/page.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
import { prisma } from '@/lib/prisma';
2-
import { createClient } from '@/utils/supabase/server';
2+
import { requireAnyRole } from '@/lib/auth';
33
import { PageHeader } from '@/components/page-header';
44
import TreinosManagementClient from './treinos-client';
55
import type { Aluno } from '@/lib/definitions';
66

77
export default async function TreinosPage() {
8-
const supabase = await createClient();
9-
const {
10-
data: { user },
11-
} = await supabase.auth.getUser();
12-
13-
if (!user) {
14-
return <div>Não autorizado</div>;
15-
}
8+
await requireAnyRole(['INSTRUTOR', 'GERENTE']);
169

1710
// Buscar todos os alunos para a seleção via Prisma
1811
const alunosPrisma = await prisma.aluno.findMany({
@@ -42,7 +35,7 @@ export default async function TreinosPage() {
4235
title="Gestão de Treinos"
4336
description="Monte treinos manualmente ou use a IA para gerar sugestões personalizadas para os alunos."
4437
/>
45-
<TreinosManagementClient initialAlunos={alunosData} instrutorId={user.id} />
38+
<TreinosManagementClient initialAlunos={alunosData} />
4639
</>
4740
);
4841
}

src/app/dashboard/treinos/treinos-client.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -316,13 +316,7 @@ function PlanoGeradoParaEdicao({
316316
);
317317
}
318318

319-
export default function TreinosManagementClient({
320-
initialAlunos,
321-
instrutorId,
322-
}: {
323-
initialAlunos: Aluno[];
324-
instrutorId: string;
325-
}) {
319+
export default function TreinosManagementClient({ initialAlunos }: { initialAlunos: Aluno[] }) {
326320
const { toast } = useToast();
327321
const [selectedAlunoId, setSelectedAlunoId] = useState<string | null>(null);
328322
const [objetivo, setObjetivo] = useState('');
@@ -381,7 +375,6 @@ export default function TreinosManagementClient({
381375
try {
382376
const res = await upsertTreinoAction({
383377
alunoId: selectedAlunoId,
384-
instrutorId,
385378
objetivo,
386379
exercicios: exercicios as Exercicio[],
387380
diaSemana: null,
@@ -430,7 +423,6 @@ export default function TreinosManagementClient({
430423
for (const workout of planoEditado.workouts) {
431424
await upsertTreinoAction({
432425
alunoId: selectedAluno.id,
433-
instrutorId,
434426
objetivo: workout.nome,
435427
exercicios: workout.exercicios.map((ex) => ({
436428
nomeExercicio: ex.nomeExercicio,

0 commit comments

Comments
 (0)