feat: intégration Sentry + migration middleware.js → proxy.js (Next.js 16)
Sentry (tracking erreurs frontend + API routes) : - sentry.client.config.js : erreurs navigateur + Session Replay sur erreurs - sentry.server.config.js : erreurs API routes (register, jwt callback) - sentry.edge.config.js : runtime edge (middleware proxy) - instrumentation.js : point d'entrée Next.js 15+ (register + captureRequestError) - next.config.mjs : wrappé avec withSentryConfig (source maps désactivés sans SENTRY_AUTH_TOKEN) - .env.sample : ajout de NEXT_PUBLIC_SENTRY_DSN (placeholder) Migration middleware → proxy (bug pré-existant surfacé par le build Sentry) : - proxy.js : fusion du rate limiting + auth NextAuth en un seul proxy Next.js 16 - middleware.js : supprimé (Next.js 16 n'accepte plus les deux fichiers simultanément) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,3 +19,6 @@ COMMENTS_PER_PAGE=5
|
|||||||
|
|
||||||
# WEBSOCKET
|
# WEBSOCKET
|
||||||
NEXT_PUBLIC_DISABLE_WEBSOCKET=false
|
NEXT_PUBLIC_DISABLE_WEBSOCKET=false
|
||||||
|
|
||||||
|
# SENTRY
|
||||||
|
NEXT_PUBLIC_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export async function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||||
|
await import('./sentry.server.config.js')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||||
|
await import('./sentry.edge.config.js')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {captureRequestError as onRequestError} from '@sentry/nextjs'
|
||||||
+17
-1
@@ -1,4 +1,5 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
import {withSentryConfig} from '@sentry/nextjs'
|
||||||
|
|
||||||
// Les URL Directus sont lues à l'exécution — elles s'adaptent à l'environnement
|
// Les URL Directus sont lues à l'exécution — elles s'adaptent à l'environnement
|
||||||
// (dev local ou production) sans rebuild.
|
// (dev local ou production) sans rebuild.
|
||||||
@@ -70,4 +71,19 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default nextConfig
|
export default withSentryConfig(nextConfig, {
|
||||||
|
// Organisation et projet Sentry pour l'upload des source maps
|
||||||
|
// Nécessite SENTRY_AUTH_TOKEN en CI pour activer l'upload
|
||||||
|
silent: !process.env.CI,
|
||||||
|
|
||||||
|
// Ne pas uploader les source maps si le token est absent (dev local)
|
||||||
|
sourcemaps: {
|
||||||
|
disable: !process.env.SENTRY_AUTH_TOKEN,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Évite d'exposer les source maps aux utilisateurs finaux
|
||||||
|
hideSourceMaps: true,
|
||||||
|
|
||||||
|
// Tunnel Sentry via notre propre domaine (contourne les adblockers)
|
||||||
|
// tunnelRoute: '/monitoring',
|
||||||
|
})
|
||||||
|
|||||||
Generated
+2226
-172
File diff suppressed because it is too large
Load Diff
+11
-2
@@ -18,6 +18,7 @@
|
|||||||
"@mui/lab": "^7.0.1-beta.20",
|
"@mui/lab": "^7.0.1-beta.20",
|
||||||
"@mui/material": "^7.3.6",
|
"@mui/material": "^7.3.6",
|
||||||
"@mui/material-nextjs": "^7.3.6",
|
"@mui/material-nextjs": "^7.3.6",
|
||||||
|
"@sentry/nextjs": "^10.48.0",
|
||||||
"@uiw/react-md-editor": "^4.0.11",
|
"@uiw/react-md-editor": "^4.0.11",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
@@ -58,9 +59,17 @@
|
|||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": "lib/__tests__/**/*.js",
|
"files": "lib/__tests__/**/*.js",
|
||||||
"envs": ["node", "es2020"],
|
"envs": [
|
||||||
|
"node",
|
||||||
|
"es2020"
|
||||||
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"camelcase": ["error", {"properties": "never"}],
|
"camelcase": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"properties": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
"capitalized-comments": "off"
|
"capitalized-comments": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,55 @@
|
|||||||
export {auth as proxy} from './auth.js'
|
import {NextResponse} from 'next/server'
|
||||||
|
import {auth} from './auth.js'
|
||||||
|
import {createRateLimiter} from '@/lib/rate-limit.js'
|
||||||
|
|
||||||
|
// 5 inscriptions max par IP toutes les 15 minutes
|
||||||
|
const checkRegister = createRateLimiter({windowMs: 15 * 60 * 1000, max: 5})
|
||||||
|
|
||||||
|
// 10 tentatives de connexion max par IP toutes les 5 minutes
|
||||||
|
const checkSignin = createRateLimiter({windowMs: 5 * 60 * 1000, max: 10})
|
||||||
|
|
||||||
|
const limiters = {
|
||||||
|
'/api/auth/register': checkRegister,
|
||||||
|
'/api/auth/callback/credentials': checkSignin,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait l'IP cliente depuis les headers HTTP.
|
||||||
|
* Priorité à X-Real-IP (Nginx), puis X-Forwarded-For.
|
||||||
|
*/
|
||||||
|
function getClientIp(request) {
|
||||||
|
const realIp = request.headers.get('x-real-ip')
|
||||||
|
if (realIp) {
|
||||||
|
return realIp.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwarded = request.headers.get('x-forwarded-for')
|
||||||
|
if (forwarded) {
|
||||||
|
return forwarded.split(',')[0].trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function proxy(request) {
|
||||||
|
const {pathname} = request.nextUrl
|
||||||
|
const check = limiters[pathname]
|
||||||
|
|
||||||
|
// Rate limiting sur les routes d'authentification critiques
|
||||||
|
if (check) {
|
||||||
|
const ip = getClientIp(request)
|
||||||
|
const result = check(`${ip}:${pathname}`)
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{message: 'Trop de tentatives. Veuillez réessayer dans quelques minutes.'},
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
headers: {'Retry-After': String(result.retryAfter)},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth(request)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import * as Sentry from '@sentry/nextjs'
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
|
||||||
|
// Taux d'échantillonnage des traces de performance (0 = désactivé, 1 = 100%)
|
||||||
|
// Valeur basse en production pour limiter le volume
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
|
||||||
|
// Capture des replays de session uniquement sur les erreurs
|
||||||
|
replaysOnErrorSampleRate: 1,
|
||||||
|
replaysSessionSampleRate: 0,
|
||||||
|
|
||||||
|
integrations: [
|
||||||
|
Sentry.replayIntegration({
|
||||||
|
// Masquer les champs sensibles dans les replays
|
||||||
|
maskAllText: false,
|
||||||
|
blockAllMedia: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Ne pas afficher les erreurs Sentry dans la console en production
|
||||||
|
debug: false,
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import * as Sentry from '@sentry/nextjs'
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
|
||||||
|
debug: false,
|
||||||
|
})
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import * as Sentry from '@sentry/nextjs'
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
|
|
||||||
|
// Taux d'échantillonnage des traces serveur
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
|
||||||
|
debug: false,
|
||||||
|
})
|
||||||
+2
-2
@@ -14,8 +14,8 @@
|
|||||||
- [x] **Tests unitaires** — Vitest sur `lib/format.js`, `lib/version-utils.js`, `lib/rate-limit.js`
|
- [x] **Tests unitaires** — Vitest sur `lib/format.js`, `lib/version-utils.js`, `lib/rate-limit.js`
|
||||||
- [x] **Tests extensions Directus** — mocks VersionsService
|
- [x] **Tests extensions Directus** — mocks VersionsService
|
||||||
- [x] **Refresh token explicite** — callback `jwt` dans NextAuth options
|
- [x] **Refresh token explicite** — callback `jwt` dans NextAuth options
|
||||||
- [ ] **Pipeline CI** — GitHub Actions (lint + test + build)
|
- [ ] **Pipeline CI** — Forgejo Actions / Woodpecker CI (lint + test + build) ⏸ en attente
|
||||||
- [ ] **Sentry** — tracking erreurs frontend + API routes
|
- [x] **Sentry** — tracking erreurs frontend + API routes
|
||||||
|
|
||||||
## Améliorations moyennes (P3)
|
## Améliorations moyennes (P3)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user