Skip to content

Commit a5ec60f

Browse files
authored
Merge pull request #133 from makcimerrr/fix/guard-planning-apis
fix(security): protéger les API planning (schedules/employees/history) sans auth
2 parents 8251cf2 + 0308426 commit a5ec60f

9 files changed

Lines changed: 84 additions & 44 deletions

File tree

app/api/employees/[id]/route.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { type NextRequest, NextResponse } from "next/server"
1+
import { NextResponse } from "next/server"
2+
import { withPlanningAccess, withPlanningEditor } from "@/lib/api/with-auth"
23
import { getEmployee, updateEmployee, deleteEmployee, emailExists } from "@/lib/db/services/employees"
34
import { validateEmployeeData } from "@/lib/db/utils"
45
import { addHistoryEntry } from '@/lib/db/services/history'
56

6-
export async function GET(request: NextRequest, context: any) {
7+
type RouteCtx = { params: Promise<{ id: string }> };
8+
9+
export const GET = withPlanningAccess<RouteCtx>(async (request, context) => {
710
const params = await context.params;
811
try {
912
const employee = await getEmployee(params.id)
@@ -15,15 +18,16 @@ export async function GET(request: NextRequest, context: any) {
1518
console.error("Error fetching employee:", error)
1619
return NextResponse.json({ error: "Failed to fetch employee" }, { status: 500 })
1720
}
18-
}
21+
})
1922

20-
export async function PUT(request: NextRequest, context: any) {
23+
export const PUT = withPlanningEditor<RouteCtx>(async (request, context) => {
2124
const params = await context.params;
2225
let currentEmployee = null;
2326
try {
2427
const data = await request.json()
25-
const userId = request.headers.get('x-user-id') || 'unknown';
26-
const userEmail = request.headers.get('x-user-email') || 'unknown';
28+
// Identité authentifiée pour l'audit (les headers x-user-* étaient falsifiables)
29+
const userId = context.user.id;
30+
const userEmail = context.user.email;
2731

2832
// Validation des données modifiées
2933
if (data.name || data.initial || data.role || data.email || data.color) {
@@ -77,13 +81,13 @@ export async function PUT(request: NextRequest, context: any) {
7781
console.error("Error updating employee:", error)
7882
return NextResponse.json({ error: "Failed to update employee" }, { status: 500 })
7983
}
80-
}
84+
})
8185

82-
export async function DELETE(request: NextRequest, context: any) {
86+
export const DELETE = withPlanningEditor<RouteCtx>(async (request, context) => {
8387
const params = await context.params;
8488
try {
85-
const userId = request.headers.get('x-user-id') || 'unknown';
86-
const userEmail = request.headers.get('x-user-email') || 'unknown';
89+
const userId = context.user.id;
90+
const userEmail = context.user.email;
8791
const currentEmployee = await getEmployee(params.id);
8892
const success = await deleteEmployee(params.id);
8993
// Audit
@@ -105,4 +109,4 @@ export async function DELETE(request: NextRequest, context: any) {
105109
console.error("Error deleting employee:", error)
106110
return NextResponse.json({ error: "Failed to delete employee" }, { status: 500 })
107111
}
108-
}
112+
})

app/api/employees/route.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
import { type NextRequest, NextResponse } from "next/server"
1+
import { NextResponse } from "next/server"
2+
import { withPlanningAccess, withPlanningEditor } from "@/lib/api/with-auth"
23
import { getEmployees, createEmployee, emailExists } from "@/lib/db/services/employees"
34
import { validateEmployeeData, getNextAvailableColor } from "@/lib/db/utils"
45
import { addHistoryEntry } from '@/lib/db/services/history'
56

6-
export async function GET() {
7+
export const GET = withPlanningAccess(async () => {
78
try {
89
const employees = await getEmployees()
910
return NextResponse.json(employees)
1011
} catch (error) {
1112
console.error("Error fetching employees:", error)
1213
return NextResponse.json({ error: "Failed to fetch employees" }, { status: 500 })
1314
}
14-
}
15+
})
1516

16-
export async function POST(request: NextRequest) {
17+
export const POST = withPlanningEditor(async (request, { user }) => {
1718
try {
1819
const data = await request.json()
19-
const userId = request.headers.get('x-user-id') || 'unknown';
20-
const userEmail = request.headers.get('x-user-email') || 'unknown';
20+
// Identité authentifiée pour l'audit (les headers x-user-* étaient falsifiables)
21+
const userId = user.id;
22+
const userEmail = user.email;
2123

2224
// Validation
2325
const errors = validateEmployeeData(data)
@@ -62,4 +64,4 @@ export async function POST(request: NextRequest) {
6264
console.error("Error creating employee:", error)
6365
return NextResponse.json({ error: "Failed to create employee" }, { status: 500 })
6466
}
65-
}
67+
})

app/api/history/route.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { NextRequest, NextResponse } from 'next/server';
1+
import { NextResponse } from 'next/server';
2+
import { withPlanningAccess } from '@/lib/api/with-auth';
23
import { getEnrichedHistory } from '@/lib/db/services/history';
34

4-
export async function GET(req: NextRequest) {
5+
export const GET = withPlanningAccess(async (req) => {
56
const { searchParams } = new URL(req.url);
67
const type = searchParams.get('type') || undefined;
78
const userId = searchParams.get('userId') || undefined;
@@ -13,4 +14,4 @@ export async function GET(req: NextRequest) {
1314
} catch (error) {
1415
return NextResponse.json({ error: 'Erreur lors de la récupération de l\'historique', details: error }, { status: 500 });
1516
}
16-
}
17+
});

app/api/schedules/absences/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { NextResponse } from "next/server";
2+
import { withPlanningAccess } from "@/lib/api/with-auth";
23
import { db } from "@/lib/db/config";
34
import { schedules } from "@/lib/db/schema/schedules";
45
import { eq, and } from "drizzle-orm";
56

67
// GET /api/schedules/absences?employeeId=...&type=...&start=YYYY-MM-DD&end=YYYY-MM-DD
7-
export async function GET(request: Request) {
8+
export const GET = withPlanningAccess(async (request) => {
89
try {
910
const { searchParams } = new URL(request.url);
1011
const employeeId = searchParams.get("employeeId");
@@ -52,4 +53,4 @@ export async function GET(request: Request) {
5253
console.error("Error fetching absences:", error);
5354
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
5455
}
55-
}
56+
});

app/api/schedules/apply-rotation/route.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { type NextRequest, NextResponse } from "next/server"
1+
import { NextResponse } from "next/server"
2+
import { withPlanningEditor } from "@/lib/api/with-auth"
23
import { upsertSchedule } from "@/lib/db/services/schedules"
34
import { getEmployees } from "@/lib/db/services/employees"
45
import { getWeekNumber } from "@/lib/db/utils"
@@ -131,7 +132,7 @@ function buildTemplates(mode: "standard" | "piscine"): Record<string, Record<str
131132
* Le cycle se répète : semaine 1 → semaine 2 → semaine 3 → semaine 1 → ...
132133
* Mode "standard" (défaut) ou "piscine" (08:00 au lieu de 09:00).
133134
*/
134-
export async function POST(request: NextRequest) {
135+
export const POST = withPlanningEditor(async (request) => {
135136
try {
136137
const { startDate, endDate, employeeIds, mode = "standard" } = await request.json()
137138

@@ -233,4 +234,4 @@ export async function POST(request: NextRequest) {
233234
{ status: 500 }
234235
)
235236
}
236-
}
237+
})

app/api/schedules/copy/route.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { type NextRequest, NextResponse } from "next/server"
1+
import { NextResponse } from "next/server"
2+
import { withPlanningEditor } from "@/lib/api/with-auth"
23
import { bulkCopySchedules } from "@/lib/db/services/schedules"
34
import { getEmployees } from "@/lib/db/services/employees"
45

5-
export async function POST(request: NextRequest) {
6+
export const POST = withPlanningEditor(async (request) => {
67
try {
78
const { employeeIds, fromWeekKey, toWeekKey } = await request.json()
89

@@ -28,4 +29,4 @@ export async function POST(request: NextRequest) {
2829
console.error("Error copying schedules:", error)
2930
return NextResponse.json({ error: "Failed to copy schedules" }, { status: 500 })
3031
}
31-
}
32+
})

app/api/schedules/range/route.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { NextRequest, NextResponse } from 'next/server';
1+
import { NextResponse } from 'next/server';
2+
import { withPlanningEditor } from '@/lib/api/with-auth';
23
import { deleteSchedule, upsertSchedule, getSchedule } from '@/lib/db/services/schedules';
34
import { getWeekNumber } from '@/lib/db/utils';
45
import { addHistoryEntry } from '@/lib/db/services/history';
@@ -15,11 +16,11 @@ function getDayNameFromDate(date: Date): string {
1516
return daysOfWeek[idx];
1617
}
1718

18-
export async function POST(req: NextRequest) {
19+
export const POST = withPlanningEditor(async (req, { user }) => {
1920
try {
2021
const { employeeId, startDate, endDate, slotType } = await req.json();
21-
const userId = req.headers.get('x-user-id') || 'unknown';
22-
const userEmail = req.headers.get('x-user-email') || 'unknown';
22+
const userId = user.id;
23+
const userEmail = user.email;
2324
if (!employeeId || !startDate || !endDate || !slotType) {
2425
return NextResponse.json({ error: 'Paramètres manquants' }, { status: 400 });
2526
}
@@ -73,4 +74,4 @@ export async function POST(req: NextRequest) {
7374
} catch (e) {
7475
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
7576
}
76-
}
77+
});

app/api/schedules/route.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NextResponse } from "next/server"
2+
import { withPlanningAccess, withPlanningEditor } from "@/lib/api/with-auth"
23
import { db } from "@/lib/db/config"
34
import { schedules } from "@/lib/db/schema/schedules"
45
import { eq, and } from "drizzle-orm"
@@ -47,7 +48,7 @@ function getDateFromWeekKeyAndDay(weekKey: string, day: string): string | null {
4748
}
4849

4950
// GET /api/schedules?weekKey=2024-W1
50-
export async function GET(request: Request) {
51+
export const GET = withPlanningAccess(async (request) => {
5152
try {
5253
const { searchParams } = new URL(request.url)
5354
const weekKey = searchParams.get("weekKey")
@@ -66,15 +67,16 @@ export async function GET(request: Request) {
6667
console.error("Error fetching schedules:", error)
6768
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
6869
}
69-
}
70+
})
7071

7172
// POST /api/schedules
72-
export async function POST(request: Request) {
73+
export const POST = withPlanningEditor(async (request, { user }) => {
7374
try {
7475
const body = await request.json()
7576
const { employeeId, weekKey, day, timeSlots } = body
76-
const userId = request.headers.get('x-user-id') || 'unknown';
77-
const userEmail = request.headers.get('x-user-email') || 'unknown';
77+
// Identité authentifiée pour l'audit (les headers x-user-* étaient falsifiables)
78+
const userId = user.id;
79+
const userEmail = user.email;
7880

7981
// Récupérer les jours fériés
8082
const holidays = await getFrenchHolidays();
@@ -148,17 +150,17 @@ export async function POST(request: Request) {
148150
console.error("Error creating/updating schedule:", error)
149151
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
150152
}
151-
}
153+
})
152154

153155
// DELETE /api/schedules?employeeId=123&weekKey=2024-W1&day=monday
154-
export async function DELETE(request: Request) {
156+
export const DELETE = withPlanningEditor(async (request, { user }) => {
155157
try {
156158
const { searchParams } = new URL(request.url)
157159
const employeeId = searchParams.get("employeeId")
158160
const weekKey = searchParams.get("weekKey")
159161
const day = searchParams.get("day")
160-
const userId = request.headers.get('x-user-id') || 'unknown';
161-
const userEmail = request.headers.get('x-user-email') || 'unknown';
162+
const userId = user.id;
163+
const userEmail = user.email;
162164

163165
if (!employeeId || !weekKey || !day) {
164166
return NextResponse.json({ error: "Missing required parameters" }, { status: 400 })
@@ -202,4 +204,4 @@ export async function DELETE(request: Request) {
202204
console.error("Error deleting schedule:", error)
203205
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
204206
}
205-
}
207+
})

lib/api/with-auth.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,30 @@ export function withAdmin<Ctx = unknown>(handler: Handler<Ctx>) {
9393
return handler(req, ctx);
9494
});
9595
}
96+
97+
/**
98+
* Lecture des données planning (employés, créneaux, historique) : admin OU
99+
* permission planning 'editor'. Les pages correspondantes sont déjà admin-only ;
100+
* ceci ferme l'accès API direct (emails/téléphones du staff, plannings).
101+
*/
102+
export function withPlanningAccess<Ctx = unknown>(handler: Handler<Ctx>) {
103+
return withAuth<Ctx>(async (req, ctx) => {
104+
if (!isAdminRole(ctx.user.role) && ctx.user.planningPermission !== 'editor') {
105+
return apiError('FORBIDDEN', 'Accès réservé au staff planning');
106+
}
107+
return handler(req, ctx);
108+
});
109+
}
110+
111+
/**
112+
* Écriture planning : permission 'editor' requise — même règle que l'UI, qui
113+
* désactive l'édition pour tout le monde (admins compris) sans 'editor'.
114+
*/
115+
export function withPlanningEditor<Ctx = unknown>(handler: Handler<Ctx>) {
116+
return withAuth<Ctx>(async (req, ctx) => {
117+
if (ctx.user.planningPermission !== 'editor') {
118+
return apiError('FORBIDDEN', 'Permission planning "editor" requise');
119+
}
120+
return handler(req, ctx);
121+
});
122+
}

0 commit comments

Comments
 (0)