diff --git a/.dockerignore b/.dockerignore index c87a625..5557979 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,11 @@ node_modules .next +admin/node_modules +admin/.next .git -.gitea -*.log -npm-debug.log* +.gitignore .env -.env.local -.env.development.local -.env.test.local -.env.production.local +.env.* +*.log dist -build +dist-ssr diff --git a/.gitignore b/.gitignore index 428e2a4..96646ac 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,8 @@ dist dist-ssr *.local .env -.env.local +.env.* +!.env.example # Editor directories and files .vscode/* diff --git a/Dockerfile b/Dockerfile index 0f1cf8f..1d49f64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM node:22-alpine -RUN apk add --no-cache libc6-compat openssl +FROM node:22.23.4-alpine +RUN apk update && apk upgrade --no-cache && apk add --no-cache libc6-compat openssl WORKDIR /app ENV PATH /app/node_modules/.bin:$PATH @@ -21,4 +21,4 @@ HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=5 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 # On lance directement via npm -CMD ["npm", "start"] \ No newline at end of file +CMD ["npm", "start"] diff --git a/admin/Dockerfile b/admin/Dockerfile index fbc1bf5..42cfdf7 100644 --- a/admin/Dockerfile +++ b/admin/Dockerfile @@ -1,11 +1,11 @@ -FROM node:22-alpine -RUN apk add --no-cache libc6-compat openssl +FROM node:22.23.4-alpine +RUN apk update && apk upgrade --no-cache && apk add --no-cache libc6-compat openssl WORKDIR /app ENV PATH /app/node_modules/.bin:$PATH # Copie et installation COPY . . -RUN npm install +RUN npm ci RUN npx prisma generate RUN npm run build @@ -21,5 +21,5 @@ USER nextjs HEALTHCHECK --interval=15s --timeout=5s --start-period=60s --retries=5 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3001/api/health || exit 1 -# On lance directement via npm (avec push de la BD si besoin, ou juste start) -CMD ["sh", "-c", "npx prisma db push --schema=prisma/schema.prisma --accept-data-loss && (node ../scripts/ensure-admin.cjs || node scripts/ensure-admin.cjs || true) && npm start"] +# On lance les migrations sans perte de données puis le serveur +CMD ["sh", "-c", "npx prisma migrate deploy --schema=prisma/schema.prisma && (node ../scripts/ensure-admin.cjs || node scripts/ensure-admin.cjs || true) && npm start"] diff --git a/admin/next.config.ts b/admin/next.config.ts index 6a94228..26ef9ad 100644 --- a/admin/next.config.ts +++ b/admin/next.config.ts @@ -1,5 +1,5 @@ import type { NextConfig } from "next"; -import path from "path"; + const nextConfig: NextConfig = { // output: 'standalone', allowedDevOrigins: ['10.0.2.2'], @@ -10,6 +10,33 @@ const nextConfig: NextConfig = { ignoreBuildErrors: true, }, + // 2. En-têtes de sécurité HTTP + async headers() { + return [ + { + source: '/(.*)', + headers: [ + { + key: 'X-Frame-Options', + value: 'DENY', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()', + }, + ], + }, + ]; + }, + // 3. (Optionnel) Pour faciliter le chargement des images si tu en as images: { remotePatterns: [ @@ -21,4 +48,4 @@ const nextConfig: NextConfig = { }, }; -export default nextConfig; \ No newline at end of file +export default nextConfig; diff --git a/admin/nixpacks.toml b/admin/nixpacks.toml index ccd5656..37a36fd 100644 --- a/admin/nixpacks.toml +++ b/admin/nixpacks.toml @@ -1,6 +1,4 @@ [phases.setup] -# Ce hash force Nixpacks à utiliser la version contenant Node 22.13.1 -nixpkgsArchive = "51ad838b03a05b1de6f9f2a0fffecee64a9788ee" nixPkgs = ["nodejs_22", "openssl"] [phases.install] @@ -10,4 +8,4 @@ cmds = ["npm ci"] cmds = ["npx prisma generate", "npm run build"] [start] -cmd = "npx prisma db push --accept-data-loss && (node ../scripts/ensure-admin.cjs || node scripts/ensure-admin.cjs || true) && npm start" \ No newline at end of file +cmd = "npx prisma migrate deploy && (node ../scripts/ensure-admin.cjs || node scripts/ensure-admin.cjs || true) && npm start" diff --git a/admin/package.json b/admin/package.json index 2613e31..cf55f91 100644 --- a/admin/package.json +++ b/admin/package.json @@ -2,6 +2,9 @@ "name": "admin", "version": "0.1.0", "private": true, + "engines": { + "node": ">=22.23.4" + }, "scripts": { "dev": "next dev", "build": "prisma generate --schema=../prisma/schema.prisma && next build", @@ -10,13 +13,13 @@ "lint": "eslint" }, "dependencies": { - "@types/bcryptjs": "^2.4.6", - "afrohub": "file:..", + "@prisma/client": "6.19.3", "bcryptjs": "^3.0.3", "d3-scale": "^4.0.2", "dotenv": "^17.4.1", "lucide-react": "^1.8.0", - "next": "16.2.3", + "next": "^16.4.3", + "nodemailer": "^10.3.1", "pg": "^8.20.0", "react": "19.2.4", "react-dom": "19.2.4", @@ -29,14 +32,15 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/d3-scale": "^4.0.9", - "@types/node": "^20", + "@types/node": "^22.14.0", + "@types/nodemailer": "^8.0.0", "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "@types/react-simple-maps": "^3.0.6", "@types/topojson-client": "^3.1.5", "eslint": "^9", - "eslint-config-next": "16.2.3", + "eslint-config-next": "^16.4.3", "tailwindcss": "^4", "typescript": "^5" } diff --git a/admin/src/app/actions/actualites.ts b/admin/src/app/actions/actualites.ts index 88540d3..dd91224 100644 --- a/admin/src/app/actions/actualites.ts +++ b/admin/src/app/actions/actualites.ts @@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { generateSlug } from "@/lib/utils"; import { ContentStatus } from "@prisma/client"; +import { requireTabPermission } from "@/lib/auth"; export async function createBlogPost(data: { title: string; @@ -22,6 +23,7 @@ export async function createBlogPost(data: { sources?: any; }) { try { + await requireTabPermission("actualites"); const slug = data.slug || generateSlug(data.title); const post = await prisma.blogPost.create({ @@ -59,6 +61,7 @@ export async function updateBlogPost(id: string, data: { sources?: any; }) { try { + await requireTabPermission("actualites"); const slug = data.slug || generateSlug(data.title); const post = await prisma.blogPost.update({ @@ -80,6 +83,7 @@ export async function updateBlogPost(id: string, data: { export async function deleteBlogPost(id: string) { try { + await requireTabPermission("actualites"); await prisma.blogPost.delete({ where: { id }, }); @@ -94,6 +98,7 @@ export async function deleteBlogPost(id: string) { export async function getBlogPosts() { try { + await requireTabPermission("actualites"); return await prisma.blogPost.findMany({ orderBy: { date: 'desc' } }); diff --git a/admin/src/app/actions/approval.ts b/admin/src/app/actions/approval.ts index 0dcbc0c..d9e6e8b 100644 --- a/admin/src/app/actions/approval.ts +++ b/admin/src/app/actions/approval.ts @@ -3,9 +3,11 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { ContentStatus } from "@prisma/client"; +import { requireTabPermission } from "@/lib/auth"; export async function approveBlogPost(id: string) { try { + await requireTabPermission("actualites"); await prisma.blogPost.update({ where: { id }, data: { status: ContentStatus.PUBLISHED } @@ -21,6 +23,7 @@ export async function approveBlogPost(id: string) { export async function approveEvent(id: string) { try { + await requireTabPermission("actualites"); await prisma.event.update({ where: { id }, data: { status: ContentStatus.PUBLISHED } @@ -36,7 +39,7 @@ export async function approveEvent(id: string) { export async function rejectBlogPost(id: string) { try { - // We could delete or archive it. Let's archive it. + await requireTabPermission("actualites"); await prisma.blogPost.update({ where: { id }, data: { status: ContentStatus.ARCHIVED } @@ -51,6 +54,7 @@ export async function rejectBlogPost(id: string) { export async function rejectEvent(id: string) { try { + await requireTabPermission("actualites"); await prisma.event.update({ where: { id }, data: { status: ContentStatus.ARCHIVED } diff --git a/admin/src/app/actions/business.ts b/admin/src/app/actions/business.ts index 9d5d09d..93a720a 100644 --- a/admin/src/app/actions/business.ts +++ b/admin/src/app/actions/business.ts @@ -2,9 +2,11 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { requireAdminSession, requireTabPermission } from "@/lib/auth"; export async function toggleVerification(id: string, currentStatus: boolean) { try { + await requireTabPermission("users"); await prisma.business.update({ where: { id }, data: { verified: !currentStatus }, @@ -24,6 +26,7 @@ export async function setFeaturedBusiness(id: string, data: { keyMetric: string; }) { try { + await requireTabPermission("users"); // 1. Réinitialiser les autres pour n'avoir qu'une seule Tête d'affiche Afroshine active await prisma.business.updateMany({ where: { isFeatured: true }, @@ -53,6 +56,7 @@ export async function setFeaturedBusiness(id: string, data: { export async function toggleHomeFeatured(id: string, currentStatus: boolean) { try { + await requireTabPermission("users"); await prisma.business.update({ where: { id }, data: { isHomeFeatured: !currentStatus }, @@ -68,6 +72,7 @@ export async function toggleHomeFeatured(id: string, currentStatus: boolean) { export async function removeFeaturedBusiness(id: string) { try { + await requireTabPermission("users"); await prisma.business.update({ where: { id }, data: { isFeatured: false }, @@ -83,6 +88,7 @@ export async function removeFeaturedBusiness(id: string) { export async function getPendingVerificationCount() { try { + await requireAdminSession(); return await prisma.business.count({ where: { isActive: true, @@ -98,11 +104,13 @@ export async function getPendingVerificationCount() { return 0; } } + export async function updateBusinessSeo(id: string, data: { metaTitle: string; metaDescription: string; }) { try { + await requireTabPermission("users"); await prisma.business.update({ where: { id }, data: { diff --git a/admin/src/app/actions/categories.ts b/admin/src/app/actions/categories.ts index 682539f..3735edd 100644 --- a/admin/src/app/actions/categories.ts +++ b/admin/src/app/actions/categories.ts @@ -2,9 +2,11 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { requireAdminSession, requireTabPermission } from "@/lib/auth"; export async function getCategories() { try { + await requireAdminSession(); return await prisma.businessCategory.findMany({ orderBy: { name: 'asc' } }); @@ -16,6 +18,7 @@ export async function getCategories() { export async function createCategory(data: { name: string; slug: string; icon?: string; suggestionId?: string }) { try { + await requireTabPermission("taxonomies"); const { suggestionId, ...categoryData } = data; const category = await prisma.businessCategory.create({ @@ -46,6 +49,7 @@ export async function createCategory(data: { name: string; slug: string; icon?: export async function updateCategory(id: string, data: { name: string; slug: string; icon?: string; isActive: boolean }) { try { + await requireTabPermission("taxonomies"); // 1. Récupérer l'ancien nom pour la migration const oldCategory = await prisma.businessCategory.findUnique({ where: { id }, @@ -77,6 +81,7 @@ export async function updateCategory(id: string, data: { name: string; slug: str export async function deleteCategory(id: string) { try { + await requireTabPermission("taxonomies"); await prisma.businessCategory.delete({ where: { id } }); @@ -91,6 +96,7 @@ export async function deleteCategory(id: string) { export async function getSuggestedCategories() { try { + await requireAdminSession(); return await prisma.business.findMany({ where: { suggestedCategory: { not: null }, diff --git a/admin/src/app/actions/comment.ts b/admin/src/app/actions/comment.ts index ab69c82..5309992 100644 --- a/admin/src/app/actions/comment.ts +++ b/admin/src/app/actions/comment.ts @@ -2,9 +2,11 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { requireTabPermission } from "@/lib/auth"; export async function deleteComment(id: string) { try { + await requireTabPermission("comments"); await prisma.comment.delete({ where: { id }, }); diff --git a/admin/src/app/actions/countries.ts b/admin/src/app/actions/countries.ts index c3e656a..f6ae6c5 100644 --- a/admin/src/app/actions/countries.ts +++ b/admin/src/app/actions/countries.ts @@ -2,9 +2,11 @@ import { prisma } from '@/lib/prisma'; import { revalidatePath } from 'next/cache'; +import { requireAdminSession, requireTabPermission } from '@/lib/auth'; export async function getCountries() { try { + await requireAdminSession(); return await prisma.country.findMany({ orderBy: { name: 'asc' } }); @@ -16,6 +18,7 @@ export async function getCountries() { export async function addCountry(data: { name: string, code: string, flag?: string, description?: string }) { try { + await requireTabPermission('countries'); const country = await prisma.country.create({ data: { name: data.name, @@ -35,6 +38,7 @@ export async function addCountry(data: { name: string, code: string, flag?: stri export async function deleteCountry(id: string) { try { + await requireTabPermission('countries'); await prisma.country.delete({ where: { id } }); @@ -48,6 +52,7 @@ export async function deleteCountry(id: string) { export async function toggleCountryStatus(id: string, isActive: boolean) { try { + await requireTabPermission('countries'); await prisma.country.update({ where: { id }, data: { isActive } @@ -62,6 +67,7 @@ export async function toggleCountryStatus(id: string, isActive: boolean) { export async function updateCountryDescription(id: string, description: string) { try { + await requireTabPermission('countries'); await prisma.country.update({ where: { id }, data: { description } diff --git a/admin/src/app/actions/event.ts b/admin/src/app/actions/event.ts index 66a8c7a..c7cd684 100644 --- a/admin/src/app/actions/event.ts +++ b/admin/src/app/actions/event.ts @@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { generateSlug } from "@/lib/utils"; import { ContentStatus } from "@prisma/client"; +import { requireTabPermission } from "@/lib/auth"; export async function createEvent(data: { title: string; @@ -22,6 +23,7 @@ export async function createEvent(data: { coverZoom?: number; }) { try { + await requireTabPermission("actualites"); const slug = data.slug || generateSlug(data.title); const event = await prisma.event.create({ @@ -59,6 +61,7 @@ export async function updateEvent(id: string, data: { coverZoom?: number; }) { try { + await requireTabPermission("actualites"); const slug = data.slug || generateSlug(data.title); const event = await prisma.event.update({ @@ -80,6 +83,7 @@ export async function updateEvent(id: string, data: { export async function deleteEvent(id: string) { try { + await requireTabPermission("actualites"); await prisma.event.delete({ where: { id }, }); diff --git a/admin/src/app/actions/interview.ts b/admin/src/app/actions/interview.ts index 16c02e8..7d83a08 100644 --- a/admin/src/app/actions/interview.ts +++ b/admin/src/app/actions/interview.ts @@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { InterviewType, ContentStatus } from "@prisma/client"; import { generateSlug } from "@/lib/utils"; +import { requireTabPermission } from "@/lib/auth"; export async function createInterview(data: { title: string; @@ -24,6 +25,7 @@ export async function createInterview(data: { status?: ContentStatus; }) { try { + await requireTabPermission("actualites"); const slug = data.slug || generateSlug(data.title); const interview = await prisma.interview.create({ @@ -62,6 +64,7 @@ export async function updateInterview(id: string, data: { status?: ContentStatus; }) { try { + await requireTabPermission("actualites"); const slug = data.slug || generateSlug(data.title); const interview = await prisma.interview.update({ @@ -82,6 +85,7 @@ export async function updateInterview(id: string, data: { export async function deleteInterview(id: string) { try { + await requireTabPermission("actualites"); await prisma.interview.delete({ where: { id }, }); diff --git a/admin/src/app/actions/legal.ts b/admin/src/app/actions/legal.ts index 0903565..5b25721 100644 --- a/admin/src/app/actions/legal.ts +++ b/admin/src/app/actions/legal.ts @@ -2,9 +2,11 @@ import { prisma } from '@/lib/prisma'; import { revalidatePath } from 'next/cache'; +import { requireAdminSession, requireTabPermission } from '@/lib/auth'; export async function getLegalDocument(type: 'CGU' | 'CGV') { try { + await requireAdminSession(); return await prisma.legalDocument.findUnique({ where: { type } }); @@ -16,6 +18,7 @@ export async function getLegalDocument(type: 'CGU' | 'CGV') { export async function updateLegalDocument(type: 'CGU' | 'CGV', title: string, content: string) { try { + await requireTabPermission('legal'); const doc = await prisma.legalDocument.upsert({ where: { type }, update: { title, content }, diff --git a/admin/src/app/actions/moderation.ts b/admin/src/app/actions/moderation.ts index c84b637..fbb189a 100644 --- a/admin/src/app/actions/moderation.ts +++ b/admin/src/app/actions/moderation.ts @@ -2,8 +2,10 @@ import { prisma } from '@/lib/prisma'; import { revalidatePath } from 'next/cache'; +import { requireTabPermission } from '@/lib/auth'; export async function getReports() { + await requireTabPermission('moderation'); return await prisma.messageReport.findMany({ include: { message: { @@ -40,6 +42,7 @@ export async function getReports() { export async function updateReportStatus(id: string, status: 'RESOLVED' | 'DISMISSED') { try { + await requireTabPermission('moderation'); await prisma.messageReport.update({ where: { id }, data: { status } @@ -52,6 +55,7 @@ export async function updateReportStatus(id: string, status: 'RESOLVED' | 'DISMI } export async function getPendingRatings() { + await requireTabPermission('moderation'); return await prisma.rating.findMany({ where: { status: 'PENDING' }, include: { @@ -68,6 +72,7 @@ export async function getPendingRatings() { export async function updateRatingStatus(id: string, status: 'APPROVED' | 'REJECTED') { try { + await requireTabPermission('moderation'); const updated = await prisma.rating.update({ where: { id }, data: { status } @@ -98,6 +103,7 @@ export async function updateRatingStatus(id: string, status: 'APPROVED' | 'REJEC export async function deleteRating(id: string) { try { + await requireTabPermission('moderation'); const rating = await prisma.rating.findUnique({ where: { id }, select: { businessId: true } @@ -129,4 +135,3 @@ export async function deleteRating(id: string) { return { success: false, error: "Erreur lors de la suppression de l'avis" }; } } - diff --git a/admin/src/app/actions/one-shot.ts b/admin/src/app/actions/one-shot.ts index dfaae5f..dcd9d6f 100644 --- a/admin/src/app/actions/one-shot.ts +++ b/admin/src/app/actions/one-shot.ts @@ -3,9 +3,11 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { OneShotType } from "@prisma/client"; +import { requireAdminSession, requireTabPermission } from "@/lib/auth"; export async function getOneShotPlans() { try { + await requireAdminSession(); return await prisma.oneShotPlan.findMany({ orderBy: { type: 'asc' } }); @@ -22,6 +24,7 @@ export async function updateOneShotPlan(type: OneShotType, data: { description?: string; }) { try { + await requireTabPermission("plans"); await prisma.oneShotPlan.upsert({ where: { type }, update: data, diff --git a/admin/src/app/actions/plans.ts b/admin/src/app/actions/plans.ts index 0155997..658b6da 100644 --- a/admin/src/app/actions/plans.ts +++ b/admin/src/app/actions/plans.ts @@ -2,9 +2,12 @@ import { prisma } from '@/lib/prisma'; import { revalidatePath } from 'next/cache'; +import { requireAdminSession, requireTabPermission } from '@/lib/auth'; +import { hasTabPermission } from '@/lib/permissions'; export async function getPricingPlans() { try { + await requireAdminSession(); return await prisma.pricingPlan.findMany({ orderBy: { offerLimit: 'asc' } }); @@ -16,6 +19,7 @@ export async function getPricingPlans() { export async function updatePricingPlanDetail(id: string, data: any) { try { + await requireTabPermission('plans'); await prisma.pricingPlan.update({ where: { id }, data: { @@ -42,6 +46,14 @@ export async function updatePricingPlanDetail(id: string, data: any) { export async function updateBusinessPlan(id: string, plan: 'STARTER' | 'BOOSTER' | 'EMPIRE') { try { + const session = await requireAdminSession(); + if ( + !hasTabPermission(session.adminRole, session.permissions, 'users') && + !hasTabPermission(session.adminRole, session.permissions, 'plans') + ) { + return { success: false, error: 'Permission refusée' }; + } + await prisma.business.update({ where: { id }, data: { plan } diff --git a/admin/src/app/actions/settings.ts b/admin/src/app/actions/settings.ts index 99d8a60..32eaa7e 100644 --- a/admin/src/app/actions/settings.ts +++ b/admin/src/app/actions/settings.ts @@ -2,9 +2,11 @@ import { prisma } from '@/lib/prisma'; import { revalidatePath } from 'next/cache'; +import { requireAdminSession, requireTabPermission } from '@/lib/auth'; export async function getSiteSettings() { try { + await requireAdminSession(); const settings = await prisma.siteSetting.findUnique({ where: { id: 'singleton' } }); @@ -66,6 +68,7 @@ export async function getSiteSettings() { export async function updateSiteSettings(data: any) { try { + await requireTabPermission('settings'); const settings = await prisma.siteSetting.upsert({ where: { id: 'singleton' }, update: data, @@ -86,5 +89,6 @@ export async function updateSiteSettings(data: any) { } export async function getExternalApiKey() { - return process.env.EXTERNAL_API_KEY || "ezfzeFZEfztZEgZEFzETGZEGZERZERF"; + await requireTabPermission('settings'); + return process.env.EXTERNAL_API_KEY || "Non configurée (définir EXTERNAL_API_KEY)"; } diff --git a/admin/src/app/actions/slides.ts b/admin/src/app/actions/slides.ts index 7644668..8696db6 100644 --- a/admin/src/app/actions/slides.ts +++ b/admin/src/app/actions/slides.ts @@ -2,9 +2,11 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { requireTabPermission } from "@/lib/auth"; export async function getSlides() { try { + await requireTabPermission("slides"); return await prisma.homeSlide.findMany({ orderBy: { order: 'asc' }, include: { business: true } @@ -17,6 +19,7 @@ export async function getSlides() { export async function createSlide(data: any) { try { + await requireTabPermission("slides"); await prisma.homeSlide.create({ data: { type: data.type, @@ -41,6 +44,7 @@ export async function createSlide(data: any) { export async function updateSlide(id: string, data: any) { try { + await requireTabPermission("slides"); await prisma.homeSlide.update({ where: { id }, data: { @@ -66,6 +70,7 @@ export async function updateSlide(id: string, data: any) { export async function deleteSlide(id: string) { try { + await requireTabPermission("slides"); await prisma.homeSlide.delete({ where: { id } }); @@ -79,6 +84,7 @@ export async function deleteSlide(id: string) { export async function toggleSlideStatus(id: string, currentStatus: boolean) { try { + await requireTabPermission("slides"); await prisma.homeSlide.update({ where: { id }, data: { isActive: !currentStatus } @@ -93,6 +99,7 @@ export async function toggleSlideStatus(id: string, currentStatus: boolean) { export async function searchBusinesses(query: string) { try { + await requireTabPermission("slides"); return await prisma.business.findMany({ where: { OR: [ diff --git a/admin/src/app/actions/suspension.ts b/admin/src/app/actions/suspension.ts index d02eca5..2b86c72 100644 --- a/admin/src/app/actions/suspension.ts +++ b/admin/src/app/actions/suspension.ts @@ -2,20 +2,23 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +import { requireTabPermission, ADMIN_SAFE_USER_SELECT } from "@/lib/auth"; /** * Suspend a user account */ export async function suspendUser(userId: string, reason: string) { try { + await requireTabPermission("users"); const user = await prisma.user.update({ where: { id: userId }, data: { isSuspended: true, suspensionReason: reason, }, - include: { - businesses: true + select: { + ...ADMIN_SAFE_USER_SELECT, + businesses: true } }); @@ -47,14 +50,16 @@ export async function suspendUser(userId: string, reason: string) { */ export async function unsuspendUser(userId: string) { try { + await requireTabPermission("users"); const user = await prisma.user.update({ where: { id: userId }, data: { isSuspended: false, suspensionReason: null, }, - include: { - businesses: true + select: { + ...ADMIN_SAFE_USER_SELECT, + businesses: true } }); @@ -85,6 +90,7 @@ export async function unsuspendUser(userId: string) { */ export async function suspendBusiness(businessId: string, reason: string) { try { + await requireTabPermission("users"); const business = await prisma.business.update({ where: { id: businessId }, data: { @@ -107,6 +113,7 @@ export async function suspendBusiness(businessId: string, reason: string) { */ export async function unsuspendBusiness(businessId: string) { try { + await requireTabPermission("users"); const business = await prisma.business.update({ where: { id: businessId }, data: { diff --git a/admin/src/app/actions/taxonomies.ts b/admin/src/app/actions/taxonomies.ts index 4b537ae..775a952 100644 --- a/admin/src/app/actions/taxonomies.ts +++ b/admin/src/app/actions/taxonomies.ts @@ -2,9 +2,11 @@ import { prisma } from '@/lib/prisma'; import { revalidatePath } from 'next/cache'; +import { requireAdminSession, requireTabPermission } from '@/lib/auth'; export async function getAllTags() { try { + await requireAdminSession(); const [blogPosts, interviews, events] = await Promise.all([ prisma.blogPost.findMany({ select: { tags: true } }), prisma.interview.findMany({ select: { tags: true } }), @@ -25,9 +27,7 @@ export async function getAllTags() { export async function deleteTagGlobally(tagName: string) { try { - // This is expensive as we need to update all records - // In a real app, a Tag model would be better - + await requireTabPermission('taxonomies'); const [blogPosts, interviews, events] = await Promise.all([ prisma.blogPost.findMany({ where: { tags: { has: tagName } } }), prisma.interview.findMany({ where: { tags: { has: tagName } } }), @@ -59,6 +59,7 @@ export async function deleteTagGlobally(tagName: string) { export async function renameTagGlobally(oldName: string, newName: string) { try { + await requireTabPermission('taxonomies'); const cleanNewName = newName.trim().toLowerCase(); if (!cleanNewName) return { success: false, error: 'Nouveau nom invalide' }; @@ -93,6 +94,7 @@ export async function renameTagGlobally(oldName: string, newName: string) { export async function getTagUsage(tagName: string) { try { + await requireTabPermission('taxonomies'); const [blogPosts, interviews, events] = await Promise.all([ prisma.blogPost.findMany({ where: { tags: { has: tagName } }, diff --git a/admin/src/app/actions/upload.ts b/admin/src/app/actions/upload.ts index c651cee..600c1a2 100644 --- a/admin/src/app/actions/upload.ts +++ b/admin/src/app/actions/upload.ts @@ -2,22 +2,85 @@ import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; +import crypto from 'crypto'; +import { requireAdminSession } from '@/lib/auth'; + +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB + +function detectImageExtension(buffer: Buffer): 'jpg' | 'png' | 'webp' | 'gif' | null { + if (buffer.length < 12) return null; + + // JPEG: FF D8 FF + if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { + return 'jpg'; + } + // PNG: 89 50 4E 47 0D 0A 1A 0A + if ( + buffer[0] === 0x89 && + buffer[1] === 0x50 && + buffer[2] === 0x4e && + buffer[3] === 0x47 && + buffer[4] === 0x0d && + buffer[5] === 0x0a && + buffer[6] === 0x1a && + buffer[7] === 0x0a + ) { + return 'png'; + } + // GIF: GIF87a or GIF89a + if ( + buffer[0] === 0x47 && + buffer[1] === 0x49 && + buffer[2] === 0x46 && + buffer[3] === 0x38 && + (buffer[4] === 0x37 || buffer[4] === 0x39) && + buffer[5] === 0x61 + ) { + return 'gif'; + } + // WebP: RIFF....WEBP + if ( + buffer[0] === 0x52 && + buffer[1] === 0x49 && + buffer[2] === 0x46 && + buffer[3] === 0x46 && + buffer[8] === 0x57 && + buffer[9] === 0x45 && + buffer[10] === 0x42 && + buffer[11] === 0x50 + ) { + return 'webp'; + } + + return null; +} export async function uploadImage(formData: FormData) { try { + await requireAdminSession(); + const file = formData.get('file') as File; if (!file) { return { success: false, error: 'Aucun fichier fourni' }; } + if (file.size > MAX_FILE_SIZE) { + return { success: false, error: 'Le fichier dépasse la taille maximale autorisée (5 Mo)' }; + } + const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); - // Create a unique filename - let fileExtension = file.name.split('.').pop()?.toLowerCase() || 'jpg'; - if (fileExtension === 'jpeg') fileExtension = 'jpg'; - - const fileName = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}.${fileExtension}`; + // Validate magic bytes & determine extension from whitelist only + const fileExtension = detectImageExtension(buffer); + if (!fileExtension) { + return { + success: false, + error: 'Format de fichier non autorisé. Seuls JPG, PNG, WebP et GIF sont acceptés.', + }; + } + + const fileName = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}.${fileExtension}`; // Path for the ROOT public/uploads (where the main site will look) const rootPublicDir = join(process.cwd(), '..', 'public', 'uploads'); diff --git a/admin/src/app/actions/user.ts b/admin/src/app/actions/user.ts index 3806e7d..6bfb7a5 100644 --- a/admin/src/app/actions/user.ts +++ b/admin/src/app/actions/user.ts @@ -1,9 +1,11 @@ "use server"; import { prisma } from "@/lib/prisma"; +import { requireTabPermission, ADMIN_SAFE_USER_SELECT } from "@/lib/auth"; export async function getClients() { try { + await requireTabPermission("users"); const clients = await prisma.user.findMany({ where: { role: { not: "ADMIN" }, @@ -12,7 +14,8 @@ export async function getClients() { { businesses: { isActive: false } } ] }, - include: { + select: { + ...ADMIN_SAFE_USER_SELECT, businesses: true }, orderBy: { @@ -28,6 +31,7 @@ export async function getClients() { export async function verifyUserEmail(userId: string) { try { + await requireTabPermission("users"); await prisma.user.update({ where: { id: userId }, data: { @@ -41,10 +45,13 @@ export async function verifyUserEmail(userId: string) { return { success: false, error: "Erreur lors de la vérification de l'email" }; } } + export async function scheduleUserDeletion(userId: string, reason: string, delayDays: number) { try { + await requireTabPermission("users"); const user = await prisma.user.findUnique({ - where: { id: userId } + where: { id: userId }, + select: ADMIN_SAFE_USER_SELECT }); if (!user) return { success: false, error: "Utilisateur non trouvé" }; @@ -99,13 +106,15 @@ export async function scheduleUserDeletion(userId: string, reason: string, delay export async function reactivateUser(userId: string, reason: string) { try { + await requireTabPermission("users"); const user = await prisma.user.update({ where: { id: userId }, data: { deletedAt: null, deletionScheduledAt: null, reactivationReason: reason - } + }, + select: ADMIN_SAFE_USER_SELECT }); const settings = await prisma.siteSetting.findUnique({ diff --git a/admin/src/app/api/cron/cleanup-users/route.ts b/admin/src/app/api/cron/cleanup-users/route.ts index e230b0c..b1c25cb 100644 --- a/admin/src/app/api/cron/cleanup-users/route.ts +++ b/admin/src/app/api/cron/cleanup-users/route.ts @@ -1,16 +1,29 @@ import { NextResponse } from "next/server"; +import crypto from "crypto"; import { prisma } from "@/lib/prisma"; +function safeCompareSecret(provided: string | null, expected: string | undefined): boolean { + if (!provided || !expected) return false; + const bufA = Buffer.from(provided); + const bufB = Buffer.from(expected); + if (bufA.length !== bufB.length) return false; + return crypto.timingSafeEqual(bufA, bufB); +} + /** * Route pour le nettoyage automatique des comptes * À appeler via une tâche cron (ex: chaque nuit à minuit) - * GET /api/cron/cleanup-users?key=VOTRE_CLE_SECRET + * Supporte `Authorization: Bearer ` ou `?key=` */ export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const key = searchParams.get("key"); + const authHeader = request.headers.get("authorization"); + const bearerToken = authHeader?.startsWith("Bearer ") + ? authHeader.slice("Bearer ".length).trim() + : null; + const key = bearerToken || searchParams.get("key"); - if (key !== process.env.CRON_SECRET) { + if (!safeCompareSecret(key, process.env.CRON_SECRET)) { return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); } diff --git a/admin/src/app/api/external/articles/route.ts b/admin/src/app/api/external/articles/route.ts index 32ab683..8f6a291 100644 --- a/admin/src/app/api/external/articles/route.ts +++ b/admin/src/app/api/external/articles/route.ts @@ -1,11 +1,20 @@ import { NextRequest, NextResponse } from 'next/server' +import crypto from 'crypto' import prisma from '@/lib/prisma' import { generateSlug } from '@/lib/utils' import { revalidatePath } from 'next/cache' +function verifyApiKey(provided: string | null, expected: string | undefined): boolean { + if (!provided || !expected) return false + const a = Buffer.from(provided) + const b = Buffer.from(expected) + if (a.length !== b.length) return false + return crypto.timingSafeEqual(a, b) +} + export async function POST(request: NextRequest) { try { - // Simple API Key authentication + // Constant-time API Key authentication const apiKey = request.headers.get('x-api-key') const expectedApiKey = process.env.EXTERNAL_API_KEY @@ -14,7 +23,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }) } - if (apiKey !== expectedApiKey) { + if (!verifyApiKey(apiKey, expectedApiKey)) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } diff --git a/admin/src/app/api/external/events/route.ts b/admin/src/app/api/external/events/route.ts index 58b9f8c..8605a0b 100644 --- a/admin/src/app/api/external/events/route.ts +++ b/admin/src/app/api/external/events/route.ts @@ -1,11 +1,20 @@ import { NextRequest, NextResponse } from 'next/server' +import crypto from 'crypto' import prisma from '@/lib/prisma' import { generateSlug } from '@/lib/utils' import { revalidatePath } from 'next/cache' +function verifyApiKey(provided: string | null, expected: string | undefined): boolean { + if (!provided || !expected) return false + const a = Buffer.from(provided) + const b = Buffer.from(expected) + if (a.length !== b.length) return false + return crypto.timingSafeEqual(a, b) +} + export async function POST(request: NextRequest) { try { - // Simple API Key authentication + // Constant-time API Key authentication const apiKey = request.headers.get('x-api-key') const expectedApiKey = process.env.EXTERNAL_API_KEY @@ -14,7 +23,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Server configuration error' }, { status: 500 }) } - if (apiKey !== expectedApiKey) { + if (!verifyApiKey(apiKey, expectedApiKey)) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } diff --git a/admin/src/app/api/health/route.ts b/admin/src/app/api/health/route.ts index 11d24d4..3ec01eb 100644 --- a/admin/src/app/api/health/route.ts +++ b/admin/src/app/api/health/route.ts @@ -1,15 +1,5 @@ import { NextResponse } from 'next/server'; -export const dynamic = 'force-dynamic'; - export async function GET() { - return NextResponse.json( - { - status: 'ok', - service: 'afrohub-admin', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - }, - { status: 200 } - ); + return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() }); } diff --git a/admin/src/app/login/page.tsx b/admin/src/app/login/page.tsx index c2758b6..1dbbb3f 100644 --- a/admin/src/app/login/page.tsx +++ b/admin/src/app/login/page.tsx @@ -43,7 +43,7 @@ function LoginForm() { toast.success(`Bienvenue, ${res.user?.name || "Administrateur"} !`); // Full page redirect ensures middleware & server cookies are freshly synchronized - window.location.href = from.startsWith("/") ? from : "/dashboard"; + window.location.href = from.startsWith("/") && !from.startsWith("//") && !from.startsWith("/\\") ? from : "/dashboard"; } catch (err) { console.error("Login client error:", err); setErrorMessage("Une erreur imprévue est survenue. Veuillez réessayer."); diff --git a/admin/src/app/users/page.tsx b/admin/src/app/users/page.tsx index 88e3970..345a47c 100644 --- a/admin/src/app/users/page.tsx +++ b/admin/src/app/users/page.tsx @@ -1,4 +1,5 @@ import { prisma } from '@/lib/prisma'; +import { ADMIN_SAFE_USER_SELECT } from '@/lib/auth'; import { getClients } from "@/app/actions/user"; import ToggleVerifyButton from '@/components/ToggleVerifyButton'; import FeaturedModal from '@/components/FeaturedModal'; @@ -17,7 +18,9 @@ async function getBusinesses() { isActive: true }, include: { - owner: true, + owner: { + select: ADMIN_SAFE_USER_SELECT + }, }, orderBy: { createdAt: 'desc', diff --git a/admin/src/lib/auth.ts b/admin/src/lib/auth.ts index 121b4dd..b6ee3f7 100644 --- a/admin/src/lib/auth.ts +++ b/admin/src/lib/auth.ts @@ -9,6 +9,28 @@ import { REMEMBER_ME_DURATION, type AdminSessionPayload, } from './session'; +import { hasTabPermission } from './permissions'; + +export const ADMIN_SAFE_USER_SELECT = { + id: true, + name: true, + email: true, + role: true, + avatar: true, + bio: true, + location: true, + phone: true, + countryId: true, + isSuspended: true, + suspensionReason: true, + emailVerified: true, + deletedAt: true, + deletionScheduledAt: true, + deletionReason: true, + reactivationReason: true, + createdAt: true, + updatedAt: true, +}; export interface AdminLoginResult { success: boolean; @@ -24,7 +46,7 @@ export interface AdminLoginResult { } /** - * Validates admin credentials (email or username + password), checks ADMIN role, + * Validates admin credentials (email + password), checks ADMIN role, * and sets the session cookie upon success with role and permissions. */ export async function loginAdmin( @@ -42,68 +64,12 @@ export async function loginAdmin( } try { - let user = null; - try { - // Search user by email (case-insensitive) or by exact name - user = await prisma.user.findFirst({ - where: { - OR: [ - { email: { equals: cleanIdentifier, mode: 'insensitive' } }, - { name: { equals: cleanIdentifier, mode: 'insensitive' } }, - ], - }, - }); - } catch (queryErr: any) { - // If column adminRole does not exist in production database yet, auto-heal schema! - if ( - queryErr?.message?.includes('adminRole') || - queryErr?.message?.includes('column') || - queryErr?.code === 'P2021' || - queryErr?.code === 'P2022' - ) { - console.log("⚠️ Colonnes manquantes détectées en production. Auto-migration de la table User..."); - try { - await prisma.$executeRawUnsafe(`ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "adminRole" TEXT DEFAULT 'ADMIN';`); - await prisma.$executeRawUnsafe(`ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "adminPermissions" TEXT[] DEFAULT ARRAY[]::TEXT[];`); - // Retry findFirst - user = await prisma.user.findFirst({ - where: { - OR: [ - { email: { equals: cleanIdentifier, mode: 'insensitive' } }, - { name: { equals: cleanIdentifier, mode: 'insensitive' } }, - ], - }, - }); - } catch (alterErr) { - console.error("Auto-alter failed:", alterErr); - } - } else { - throw queryErr; - } - } - - // Auto-bootstrap for production: If admin@afrohub.com is requested and does not exist in DB yet, - // create it automatically so that newly deployed production databases have a Super Admin immediately. - if (!user && (cleanIdentifier.toLowerCase() === 'admin@afrohub.com' || cleanIdentifier.toLowerCase() === 'admin')) { - try { - const initialPassword = process.env.ADMIN_INITIAL_PASSWORD || 'afrohub2025'; - const hashedPassword = await bcrypt.hash(initialPassword, 10); - user = await prisma.user.create({ - data: { - name: 'Admin Afrohub', - email: 'admin@afrohub.com', - password: hashedPassword, - role: 'ADMIN', - adminRole: 'SUPER_ADMIN', - adminPermissions: [], - emailVerified: true, - }, - }); - console.log('✅ Compte Super Admin auto-initialisé avec succès lors de la première connexion.'); - } catch (e) { - console.error('Erreur auto-création super admin:', e); - } - } + // Search user by unique email (case-insensitive) + const user = await prisma.user.findFirst({ + where: { + email: { equals: cleanIdentifier, mode: 'insensitive' }, + }, + }); if (!user) { return { @@ -219,6 +185,17 @@ export async function requireAdminSession(): Promise { return session; } +/** + * Helper to ensure a server action is invoked by an admin with permission for a specific tab. + */ +export async function requireTabPermission(tabKey: string): Promise { + const session = await requireAdminSession(); + if (!hasTabPermission(session.adminRole, session.permissions, tabKey)) { + throw new Error(`Non autorisé : permission '${tabKey}' requise`); + } + return session; +} + /** * Helper to ensure the current admin has SUPER_ADMIN role. */ @@ -229,4 +206,3 @@ export async function requireSuperAdminSession(): Promise { } return session; } - diff --git a/admin/src/lib/mail.ts b/admin/src/lib/mail.ts index 8b5c0ee..330f0d8 100644 --- a/admin/src/lib/mail.ts +++ b/admin/src/lib/mail.ts @@ -1,11 +1,5 @@ import nodemailer from 'nodemailer'; -console.log('SMTP Config:', { - host: process.env.SMTP_HOST, - port: process.env.SMTP_PORT, - secure: process.env.SMTP_SECURE, - user: process.env.SMTP_USER -}); const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT || '587'), @@ -15,7 +9,7 @@ const transporter = nodemailer.createTransport({ pass: process.env.SMTP_PASS, }, tls: { - rejectUnauthorized: false + rejectUnauthorized: process.env.NODE_ENV === 'production' } }); @@ -29,7 +23,6 @@ export const sendEmail = async ({ to, subject, html }: { to: string; subject: st try { const info = await transporter.sendMail(mailOptions); - console.log('Email sent: ' + info.response); return info; } catch (error) { console.error('Error sending email:', error); diff --git a/admin/src/lib/prisma.ts b/admin/src/lib/prisma.ts index 1100fd0..8e1293a 100644 --- a/admin/src/lib/prisma.ts +++ b/admin/src/lib/prisma.ts @@ -1,19 +1,25 @@ import { PrismaClient } from '@prisma/client' -const connectionString = process.env.DATABASE_URL || "postgresql://admin:adminpassword@192.168.1.25:5432/afrohub_dev?schema=public" +const connectionString = process.env.DATABASE_URL + +if (!connectionString && process.env.NODE_ENV === 'production') { + throw new Error('CRITICAL: DATABASE_URL environment variable must be set in production.') +} const globalForPrisma = globalThis as unknown as { prisma_admin_native?: PrismaClient } -export const prisma = globalForPrisma.prisma_admin_native || new PrismaClient({ - datasources: { - db: { - url: connectionString, - }, - }, -}) +export const prisma = globalForPrisma.prisma_admin_native || new PrismaClient( + connectionString + ? { + datasources: { + db: { + url: connectionString, + }, + }, + } + : undefined +) if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma_admin_native = prisma export default prisma - - diff --git a/admin/src/lib/session.ts b/admin/src/lib/session.ts index 8488aca..e9de19e 100644 --- a/admin/src/lib/session.ts +++ b/admin/src/lib/session.ts @@ -15,10 +15,20 @@ export interface AdminSessionPayload { iat: number; } -const JWT_SECRET = - process.env.ADMIN_JWT_SECRET || - process.env.NEXTAUTH_SECRET || - 'afrohub_admin_secure_jwt_token_secret_key_2025_9876543210'; +function getJwtSecret(): string { + const secret = + process.env.ADMIN_JWT_SECRET || + process.env.NEXTAUTH_SECRET || + process.env.JWT_SECRET; + + if (!secret) { + if (process.env.NODE_ENV === 'production') { + throw new Error('CRITICAL: ADMIN_JWT_SECRET or NEXTAUTH_SECRET must be configured in production.'); + } + return 'dev_only_afrohub_admin_jwt_secret_change_in_prod'; + } + return secret; +} function base64UrlEncode(str: string): string { if (typeof Buffer !== 'undefined') { @@ -61,7 +71,7 @@ async function getCryptoKey(): Promise { const enc = new TextEncoder(); return await crypto.subtle.importKey( 'raw', - enc.encode(JWT_SECRET), + enc.encode(getJwtSecret()), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify'] diff --git a/admin/src/middleware.ts b/admin/src/middleware.ts index d5275d5..f846119 100644 --- a/admin/src/middleware.ts +++ b/admin/src/middleware.ts @@ -15,11 +15,13 @@ const PUBLIC_PREFIXES = [ export async function middleware(request: NextRequest) { const { pathname, search } = request.nextUrl; + const isServerAction = request.headers.has('next-action'); - // 1. Bypass static files and Next.js internal assets + // 1. Bypass static files and Next.js internal assets (only for GET/HEAD, never for Server Action POSTs) if ( - pathname.startsWith('/_next') || - pathname.includes('.') // any file with an extension (e.g. favicon.ico, images, robots.txt) + !isServerAction && + (request.method === 'GET' || request.method === 'HEAD') && + (pathname.startsWith('/_next') || pathname.includes('.')) ) { return NextResponse.next(); } @@ -32,21 +34,24 @@ export async function middleware(request: NextRequest) { // 3. If accessing /login if (isLoginPage) { - // If already logged in, redirect directly to dashboard - if (session) { + // If already logged in and not invoking a login server action, redirect directly to dashboard + if (session && !isServerAction) { return NextResponse.redirect(new URL('/dashboard', request.url)); } return NextResponse.next(); } - // 4. If accessing other public APIs (like cron or external integrations) + // 4. If accessing other public APIs (like cron or external integrations), disallow Server Action invocations const isPublicPrefix = PUBLIC_PREFIXES.some(prefix => pathname.startsWith(prefix)); - if (isPublicPrefix) { + if (isPublicPrefix && !isServerAction) { return NextResponse.next(); } - // 5. Protected route: redirect to /login if unauthenticated + // 5. Protected route: redirect to /login if unauthenticated (or 401 for Server Actions) if (!session) { + if (isServerAction) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } const loginUrl = new URL('/login', request.url); const returnUrl = pathname + (search || ''); if (pathname !== '/' && pathname !== '') { @@ -58,6 +63,9 @@ export async function middleware(request: NextRequest) { // 6. Check granular tab permission const tabKey = getTabForPathname(pathname); if (tabKey && !hasTabPermission(session.adminRole, session.permissions, tabKey)) { + if (isServerAction) { + return NextResponse.json({ error: 'Permission refusée' }, { status: 403 }); + } // If user lacks permission for this tab, redirect back to /dashboard const dashboardUrl = new URL('/dashboard', request.url); dashboardUrl.searchParams.set('unauthorized', tabKey); diff --git a/app/actions/auth.ts b/app/actions/auth.ts index 5a762fb..9e1e0cc 100644 --- a/app/actions/auth.ts +++ b/app/actions/auth.ts @@ -3,8 +3,23 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + export async function reactivateUserAccount(userId: string, reason: string) { try { + if (!userId || typeof reason !== "string" || reason.trim().length === 0) { + return { success: false, error: "Paramètres invalides" }; + } + + const sanitizedReason = reason.trim().slice(0, 1000); + const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, name: true, email: true, deletedAt: true } @@ -23,24 +38,24 @@ export async function reactivateUserAccount(userId: string, reason: string) { data: { deletedAt: null, deletionScheduledAt: null, - reactivationReason: reason + reactivationReason: sanitizedReason } }); // Optionnel : Envoyer un mail de confirmation const settings = await prisma.siteSetting.findUnique({ where: { id: "singleton" } }); if (settings?.reactivateAccountTemplate) { - const { sendEmail } = await import("@/lib/mail"); - const html = settings.reactivateAccountTemplate - .replace(/{name}/g, user.name) - .replace(/{siteName}/g, settings.siteName) - .replace(/{reason}/g, reason); - - await sendEmail({ - to: user.email, - subject: settings.reactivateAccountSubject, - html - }); + const { sendEmail } = await import("@/lib/mail"); + const html = settings.reactivateAccountTemplate + .replace(/{name}/g, escapeHtml(user.name || "")) + .replace(/{siteName}/g, escapeHtml(settings.siteName || "Afrohub")) + .replace(/{reason}/g, escapeHtml(sanitizedReason)); + + await sendEmail({ + to: user.email, + subject: settings.reactivateAccountSubject, + html + }); } revalidatePath("/"); diff --git a/app/actions/events.ts b/app/actions/events.ts index 92c8e57..f986afd 100644 --- a/app/actions/events.ts +++ b/app/actions/events.ts @@ -3,6 +3,9 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { ContentStatus } from "@prisma/client"; +import { getAuthUser } from "@/lib/auth-utils"; +import { sanitizeHTML } from "@/lib/sanitize"; +import { safeUrl } from "@/lib/utils"; export async function createEvent(data: { title: string; @@ -14,20 +17,33 @@ export async function createEvent(data: { tags?: string[]; }) { try { + const authUser = await getAuthUser(); + if (!authUser) { + return { success: false, error: "Authentification requise" }; + } + const slug = data.title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, ''); + let validatedLink: string | null = null; + if (data.link && data.link.trim()) { + const checked = safeUrl(data.link.trim()); + if (checked !== '#') { + validatedLink = checked; + } + } + const event = await prisma.event.create({ data: { - title: data.title, - description: data.description, + title: data.title.trim().slice(0, 200), + description: sanitizeHTML(data.description || ''), date: new Date(data.date), - location: data.location, + location: data.location.trim().slice(0, 200), thumbnailUrl: data.thumbnailUrl, - link: data.link || null, - tags: data.tags || [], + link: validatedLink, + tags: Array.isArray(data.tags) ? data.tags.slice(0, 20) : [], status: 'PENDING' as ContentStatus, // Always pending for user submission slug: `${slug}-${Math.random().toString(36).substring(2, 7)}` } diff --git a/app/actions/news.ts b/app/actions/news.ts index 3798185..a799b6b 100644 --- a/app/actions/news.ts +++ b/app/actions/news.ts @@ -3,6 +3,8 @@ import { prisma } from "@/lib/prisma"; import { revalidatePath } from "next/cache"; import { ContentStatus } from "@prisma/client"; +import { getAuthUser } from "@/lib/auth-utils"; +import { sanitizeHTML } from "@/lib/sanitize"; export async function createNews(data: { title: string; @@ -14,6 +16,11 @@ export async function createNews(data: { sources?: any; }) { try { + const authUser = await getAuthUser(); + if (!authUser) { + return { success: false, error: "Authentification requise" }; + } + const slug = data.title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') @@ -21,12 +28,12 @@ export async function createNews(data: { const post = await prisma.blogPost.create({ data: { - title: data.title, - excerpt: data.excerpt, - content: data.content, - author: data.author, + title: data.title.trim().slice(0, 200), + excerpt: data.excerpt.trim().slice(0, 500), + content: sanitizeHTML(data.content || ''), + author: data.author.trim().slice(0, 100), imageUrl: data.imageUrl, - tags: data.tags || [], + tags: Array.isArray(data.tags) ? data.tags.slice(0, 20) : [], sources: data.sources || [], status: 'PENDING' as ContentStatus, // Always pending for user submission slug: `${slug}-${Math.random().toString(36).substring(2, 7)}` diff --git a/app/actions/upload.ts b/app/actions/upload.ts index a3960c9..8ae0123 100644 --- a/app/actions/upload.ts +++ b/app/actions/upload.ts @@ -1,34 +1,104 @@ 'use server'; +import { getAuthUser } from '@/lib/auth-utils'; + +const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB + +function detectSafeImageMimeType(buffer: Buffer): string | null { + if (buffer.length < 12) return null; + + // JPEG: FF D8 FF + if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { + return 'image/jpeg'; + } + + // PNG: 89 50 4E 47 0D 0A 1A 0A + if ( + buffer[0] === 0x89 && + buffer[1] === 0x50 && + buffer[2] === 0x4e && + buffer[3] === 0x47 && + buffer[4] === 0x0d && + buffer[5] === 0x0a && + buffer[6] === 0x1a && + buffer[7] === 0x0a + ) { + return 'image/png'; + } + + // GIF: GIF87a or GIF89a + if ( + buffer[0] === 0x47 && + buffer[1] === 0x49 && + buffer[2] === 0x46 && + buffer[3] === 0x38 && + (buffer[4] === 0x37 || buffer[4] === 0x39) && + buffer[5] === 0x61 + ) { + return 'image/gif'; + } + + // WebP: RIFF....WEBP + if ( + buffer[0] === 0x52 && + buffer[1] === 0x49 && + buffer[2] === 0x46 && + buffer[3] === 0x46 && + buffer[8] === 0x57 && + buffer[9] === 0x45 && + buffer[10] === 0x42 && + buffer[11] === 0x50 + ) { + return 'image/webp'; + } + + return null; +} + export async function uploadImage(formData: FormData) { try { + const authUser = await getAuthUser(); + if (!authUser) { + return { success: false, error: 'Authentification requise' }; + } + const file = formData.get('file') as File; if (!file) { return { success: false, error: 'Aucun fichier fourni' }; } + if (file.size > MAX_UPLOAD_SIZE_BYTES) { + return { success: false, error: 'Le fichier dépasse la taille maximale autorisée (5 Mo)' }; + } + // Convert file to buffer const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); + if (buffer.length > MAX_UPLOAD_SIZE_BYTES) { + return { success: false, error: 'Le fichier dépasse la taille maximale autorisée (5 Mo)' }; + } + + // Derive MIME type strictly from binary magic bytes (rejects SVG/HTML/scripts) + const mimeType = detectSafeImageMimeType(buffer); + if (!mimeType) { + return { + success: false, + error: 'Format de fichier non autorisé (seuls JPG, PNG, WebP et GIF sont acceptés)', + }; + } + // Convert to Base64 const base64 = buffer.toString('base64'); - let mimeType = file.type || 'image/jpeg'; - - // Normalize JPEG mime types - if (mimeType === 'image/jpg' || mimeType === 'image/pjpeg') { - mimeType = 'image/jpeg'; - } - const dataUrl = `data:${mimeType};base64,${base64}`; // Return the data URL directly (it will be saved in the database) - return { - success: true, - url: dataUrl + return { + success: true, + url: dataUrl, }; } catch (error) { console.error('Error during image conversion:', error); - return { success: false, error: 'Erreur lors de la conversion de l\'image' }; + return { success: false, error: "Erreur lors de la conversion de l'image" }; } } diff --git a/app/actualites/[id]/page.tsx b/app/actualites/[id]/page.tsx index a0ce78a..247f3f6 100644 --- a/app/actualites/[id]/page.tsx +++ b/app/actualites/[id]/page.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { notFound } from 'next/navigation'; import { ArrowLeft, ArrowRight, User, Calendar, Share2 } from 'lucide-react'; import { sanitizeHTML } from '../../../lib/sanitize'; +import { safeUrl } from '../../../lib/utils'; import { prisma } from '../../../lib/prisma'; import BlogShareButtons from '../../../components/BlogShareButtons'; import LightboxImage from '@/components/LightboxImage'; @@ -182,7 +183,7 @@ export default async function BlogPostPage({ params }: Props) {
  • Participer à l'événement diff --git a/app/annuaire/[id]/BusinessDetailClient.tsx b/app/annuaire/[id]/BusinessDetailClient.tsx index 461ea4e..903203a 100644 --- a/app/annuaire/[id]/BusinessDetailClient.tsx +++ b/app/annuaire/[id]/BusinessDetailClient.tsx @@ -7,6 +7,7 @@ import { ArrowLeft, MapPin, Globe, Mail, Phone, CheckCircle, Star, Facebook, Lin import { Business, OfferType, Rating } from '@/types'; import { toast } from 'react-hot-toast'; import { useUser } from '@/components/UserProvider'; +import { safeUrl } from '@/lib/utils'; interface Props { initialBusiness: Business; @@ -614,22 +615,22 @@ const BusinessDetailClient = ({ initialBusiness }: Props) => {

    Réseaux Sociaux

    {business.socialLinks.facebook && ( - + )} {business.socialLinks.linkedin && ( - + )} {business.socialLinks.instagram && ( - + )} {business.socialLinks.twitter && ( - + )} diff --git a/app/api/admin/reports/[id]/route.ts b/app/api/admin/reports/[id]/route.ts index 4121fd7..58c1df2 100644 --- a/app/api/admin/reports/[id]/route.ts +++ b/app/api/admin/reports/[id]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '../../../../../lib/prisma' +import { isAdmin } from '@/lib/auth-utils' // PATCH /api/admin/reports/[id] — Update report status export async function PATCH( @@ -7,21 +8,10 @@ export async function PATCH( context: { params: Promise<{ id: string }> } ): Promise { try { - const userId = request.headers.get('x-user-id') const { id } = await context.params const { status } = await request.json() - if (!userId) { - return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) - } - - // Check if user is admin - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { role: true } - }) - - if (!user || user.role !== 'ADMIN') { + if (!(await isAdmin(request))) { return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 }) } diff --git a/app/api/admin/reports/route.ts b/app/api/admin/reports/route.ts index f8174bb..613df6f 100644 --- a/app/api/admin/reports/route.ts +++ b/app/api/admin/reports/route.ts @@ -1,22 +1,11 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' +import { isAdmin } from '@/lib/auth-utils' // GET /api/admin/reports — List all reports for moderation export async function GET(request: NextRequest) { try { - const userId = request.headers.get('x-user-id') - - if (!userId) { - return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) - } - - // Check if user is admin - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { role: true } - }) - - if (!user || user.role !== 'ADMIN') { + if (!(await isAdmin(request))) { return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 }) } diff --git a/app/api/analytics/businesses/[id]/route.ts b/app/api/analytics/businesses/[id]/route.ts index 144bd04..45008f2 100644 --- a/app/api/analytics/businesses/[id]/route.ts +++ b/app/api/analytics/businesses/[id]/route.ts @@ -1,17 +1,37 @@ import { NextRequest, NextResponse } from 'next/server'; import prisma from '../../../../../lib/prisma'; +import { getAuthUser } from '@/lib/auth-utils'; export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { + const authUser = await getAuthUser(request); + if (!authUser) { + return NextResponse.json({ error: 'Authentification requise' }, { status: 401 }); + } + const { id } = await params; if (!id) { return NextResponse.json({ error: 'ID Business requis' }, { status: 400 }); } + // Verify ownership or ADMIN role + const business = await prisma.business.findUnique({ + where: { id }, + select: { id: true, ownerId: true } + }); + + if (!business) { + return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 }); + } + + if (business.ownerId !== authUser.id && authUser.role !== 'ADMIN') { + return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 }); + } + // 1. Get total views from AnalyticsEvent const totalViews = await prisma.analyticsEvent.count({ where: { diff --git a/app/api/analytics/route.ts b/app/api/analytics/route.ts index a9b072e..4b7c342 100644 --- a/app/api/analytics/route.ts +++ b/app/api/analytics/route.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; +import { isIP } from 'node:net'; import prisma from '@/lib/prisma'; import { getGeoInfo } from '@/lib/geo'; @@ -7,14 +8,15 @@ export async function POST(request: NextRequest) { const body = await request.json(); const { type, path, label, value, metadata } = body; - if (!type || !path) { + if (!type || !path || typeof type !== 'string' || typeof path !== 'string') { return NextResponse.json({ error: 'Type et Path requis' }, { status: 400 }); } // Extract detailed info from headers - const userAgent = request.headers.get('user-agent') || ''; - const referrer = request.headers.get('referer') || ''; - const ip = request.headers.get('x-forwarded-for')?.split(',')[0] || '127.0.0.1'; + const userAgent = (request.headers.get('user-agent') || '').slice(0, 500); + const referrer = (request.headers.get('referer') || '').slice(0, 500); + const rawForwardedIp = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || '127.0.0.1'; + const ip = isIP(rawForwardedIp) !== 0 ? rawForwardedIp : '127.0.0.1'; // Basic User-Agent parsing const isMobile = /mobile/i.test(userAgent); @@ -33,22 +35,22 @@ export async function POST(request: NextRequest) { else if (/iphone|ipad/i.test(userAgent)) os = 'iOS'; // Simple Country detection - const { country, city } = await getGeoInfo(ip, request.headers); + const { country } = await getGeoInfo(ip, request.headers); const event = await prisma.analyticsEvent.create({ data: { - type, - path, - label: label || null, - value: value || null, - metadata: metadata || {}, + type: type.slice(0, 100), + path: path.slice(0, 500), + label: typeof label === 'string' ? label.slice(0, 200) : null, + value: typeof value === 'number' ? value : null, + metadata: metadata && typeof metadata === 'object' ? metadata : {}, ip, country, browser, os, device, referrer, - userId: metadata?.userId || null, + userId: typeof metadata?.userId === 'string' ? metadata.userId : null, }, }); diff --git a/app/api/auth/forgot-password/route.ts b/app/api/auth/forgot-password/route.ts index ae7f234..c468bcc 100644 --- a/app/api/auth/forgot-password/route.ts +++ b/app/api/auth/forgot-password/route.ts @@ -5,6 +5,15 @@ import crypto from 'crypto'; import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf'; +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + export async function POST(req: NextRequest) { // CSRF Protection const isOriginValid = verifyOrigin(req); @@ -52,13 +61,14 @@ export async function POST(req: NextRequest) { where: { id: 'singleton' } }); - const siteName = settings?.siteName || 'Afrohub'; + const siteName = escapeHtml(settings?.siteName || 'Afrohub'); + const safeUserName = escapeHtml(user.name || ''); const subject = settings?.resetPasswordSubject || 'Réinitialisation de votre mot de passe - Afrohub'; let html = settings?.resetPasswordTemplate || `

    ${siteName}

    -

    Bonjour ${user.name},

    +

    Bonjour ${safeUserName},

    Vous avez demandé la réinitialisation de votre mot de passe. Cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe :

    Réinitialiser mon mot de passe @@ -71,7 +81,7 @@ export async function POST(req: NextRequest) { // Replace placeholders html = html - .replace(/{name}/g, user.name) + .replace(/{name}/g, safeUserName) .replace(/{resetUrl}/g, resetUrl) .replace(/{siteName}/g, siteName); diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 925007b..4e07874 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -3,6 +3,7 @@ import prisma from '@/lib/prisma'; import bcrypt from 'bcryptjs'; import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf'; +import { signUserToken, setUserSessionCookie } from '@/lib/auth-utils'; export async function POST(request: NextRequest) { // CSRF Protection @@ -75,11 +76,34 @@ export async function POST(request: NextRequest) { ); } - // Omit password from response - const { password: _, ...userWithoutPassword } = user; + // Sign and set HttpOnly session cookie + const token = await signUserToken({ + userId: user.id, + email: user.email, + role: user.role, + }); + await setUserSessionCookie(token); + + // Return only safe user fields (never leak password, resetToken, verificationToken) + const safeUser = { + id: user.id, + name: user.name, + email: user.email, + role: user.role, + avatar: user.avatar, + bio: user.bio, + location: user.location, + phone: user.phone, + countryId: user.countryId, + isSuspended: user.isSuspended, + suspensionReason: user.suspensionReason, + emailVerified: user.emailVerified, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }; return NextResponse.json( - { message: 'Connexion réussie', user: userWithoutPassword }, + { message: 'Connexion réussie', user: safeUser }, { status: 200 } ); diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..3106c0b --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from 'next/server'; +import { clearUserSessionCookie } from '@/lib/auth-utils'; + +export async function POST() { + try { + await clearUserSessionCookie(); + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Logout error:', error); + return NextResponse.json({ error: 'Erreur lors de la déconnexion' }, { status: 500 }); + } +} + diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index fa779b6..8881f11 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -5,6 +5,15 @@ import crypto from 'crypto'; import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf'; +function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + export async function POST(request: NextRequest) { // CSRF Protection const isOriginValid = verifyOrigin(request); @@ -19,7 +28,7 @@ export async function POST(request: NextRequest) { try { const rawBody = await request.json(); - const name = rawBody.name; + const name = typeof rawBody.name === 'string' ? rawBody.name.trim() : ''; const email = rawBody.email?.toLowerCase().trim(); const password = rawBody.password; @@ -31,9 +40,9 @@ export async function POST(request: NextRequest) { ); } - if (password.length < 6) { + if (password.length < 8) { return NextResponse.json( - { error: 'Le mot de passe doit contenir au moins 6 caractères' }, + { error: 'Le mot de passe doit contenir au moins 8 caractères' }, { status: 400 } ); } @@ -74,14 +83,16 @@ export async function POST(request: NextRequest) { where: { id: 'singleton' } }); - const siteName = settings?.siteName || 'Afrohub'; + const safeName = escapeHtml(name); + const safeEmail = escapeHtml(email); + const siteName = escapeHtml(settings?.siteName || 'Afrohub'); const subject = settings?.verifyEmailSubject || 'Vérification de votre compte - Afrohub'; const verifyUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/auth/verify?token=${verificationToken}`; let html = settings?.verifyEmailTemplate || `

    ${siteName}

    -

    Bonjour ${name},

    +

    Bonjour ${safeName},

    Merci de vous être inscrit sur ${siteName}. Veuillez vérifier votre adresse email en cliquant sur le bouton ci-dessous :

    Vérifier mon compte @@ -89,9 +100,9 @@ export async function POST(request: NextRequest) {
    `; - // Replace placeholders + // Replace placeholders with HTML-escaped values html = html - .replace(/{name}/g, name) + .replace(/{name}/g, safeName) .replace(/{verifyUrl}/g, verifyUrl) .replace(/{siteName}/g, siteName); @@ -107,7 +118,7 @@ export async function POST(request: NextRequest) { // Send admin notification email try { const adminEmail = settings?.contactEmail || 'support@afrohub.com'; - const adminSubject = `[Admin] Nouvelle inscription sur ${siteName} : ${name}`; + const adminSubject = `[Admin] Nouvelle inscription sur ${settings?.siteName || 'Afrohub'} : ${name}`; const adminHtml = `

    Notification Admin - Inscription

    @@ -116,11 +127,11 @@ export async function POST(request: NextRequest) { - + - + @@ -149,11 +160,25 @@ export async function POST(request: NextRequest) { console.error('Failed to send registration email(s):', mailError); } - // Omit password from response - const { password: _, ...userWithoutPassword } = user; + // Return only safe user fields (never leak password, verificationToken, resetToken) + const safeUser = { + id: user.id, + name: user.name, + email: user.email, + role: user.role, + avatar: user.avatar, + bio: user.bio, + location: user.location, + phone: user.phone, + countryId: user.countryId, + isSuspended: user.isSuspended, + emailVerified: user.emailVerified, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }; return NextResponse.json( - { message: 'Compte créé avec succès', user: userWithoutPassword }, + { message: 'Compte créé avec succès', user: safeUser }, { status: 201 } ); diff --git a/app/api/auth/resend-verification/route.ts b/app/api/auth/resend-verification/route.ts index 662d483..9305c7b 100644 --- a/app/api/auth/resend-verification/route.ts +++ b/app/api/auth/resend-verification/route.ts @@ -4,6 +4,15 @@ import crypto from 'crypto'; import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf'; +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + export async function POST(request: NextRequest) { // CSRF Protection const isOriginValid = verifyOrigin(request); @@ -61,7 +70,8 @@ export async function POST(request: NextRequest) { where: { id: 'singleton' } }); - const siteName = settings?.siteName || 'Afrohub'; + const siteName = escapeHtml(settings?.siteName || 'Afrohub'); + const safeUserName = escapeHtml(user.name || ''); const subject = settings?.verifyEmailSubject || 'Vérification de votre compte - Afrohub'; const verifyUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/auth/verify?token=${verificationToken}`; @@ -78,7 +88,7 @@ export async function POST(request: NextRequest) { // Replace placeholders html = html - .replace(/{name}/g, user.name) + .replace(/{name}/g, safeUserName) .replace(/{verifyUrl}/g, verifyUrl) .replace(/{siteName}/g, siteName); diff --git a/app/api/auth/reset-password/route.ts b/app/api/auth/reset-password/route.ts index 36b1366..35be0b7 100644 --- a/app/api/auth/reset-password/route.ts +++ b/app/api/auth/reset-password/route.ts @@ -18,10 +18,17 @@ export async function POST(request: NextRequest) { try { const { token, password } = await request.json(); - if (!token || !password) { + if (!token || !password || typeof password !== 'string') { return NextResponse.json({ error: 'Token et mot de passe sont requis' }, { status: 400 }); } + if (password.length < 8) { + return NextResponse.json( + { error: 'Le mot de passe doit contenir au moins 8 caractères' }, + { status: 400 } + ); + } + const user = await prisma.user.findFirst({ where: { resetToken: token, diff --git a/app/api/blog/[id]/route.ts b/app/api/blog/[id]/route.ts index 249de41..59fa5ae 100644 --- a/app/api/blog/[id]/route.ts +++ b/app/api/blog/[id]/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '../../../../lib/prisma' - -type Params = { params: Promise<{ id: string }> } +import { isAdmin } from '@/lib/auth-utils' // GET /api/blog/[id] export async function GET( @@ -19,12 +18,16 @@ export async function GET( } } -// PATCH /api/blog/[id] +// PATCH /api/blog/[id] (Admin only) export async function PATCH( request: NextRequest, context: { params: Promise<{ id: string }> } ): Promise { try { + if (!(await isAdmin(request))) { + return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 }) + } + const { id } = await context.params const body = await request.json() if (body.title && body.slug === undefined) { @@ -39,12 +42,16 @@ export async function PATCH( } } -// DELETE /api/blog/[id] +// DELETE /api/blog/[id] (Admin only) export async function DELETE( - _request: NextRequest, + request: NextRequest, context: { params: Promise<{ id: string }> } ): Promise { try { + if (!(await isAdmin(request))) { + return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 }) + } + const { id } = await context.params await prisma.blogPost.delete({ where: { id } }) return NextResponse.json({ success: true }) diff --git a/app/api/blog/route.ts b/app/api/blog/route.ts index cc03ddd..e938303 100644 --- a/app/api/blog/route.ts +++ b/app/api/blog/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' import { generateSlug } from '@/lib/utils' +import { isAdmin } from '@/lib/auth-utils' // GET /api/blog export async function GET() { @@ -21,9 +22,13 @@ export async function GET() { } } -// POST /api/blog +// POST /api/blog (Admin only) export async function POST(request: NextRequest) { try { + if (!(await isAdmin(request))) { + return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 }) + } + const body = await request.json() if (!body.slug && body.title) { body.slug = generateSlug(body.title) diff --git a/app/api/businesses/[id]/rate/route.ts b/app/api/businesses/[id]/rate/route.ts index 6f05e7c..3ff0653 100644 --- a/app/api/businesses/[id]/rate/route.ts +++ b/app/api/businesses/[id]/rate/route.ts @@ -1,17 +1,20 @@ import { NextRequest, NextResponse } from 'next/server'; import prisma from '@/lib/prisma'; +import { getAuthUser } from '@/lib/auth-utils'; export async function POST( req: NextRequest, context: { params: Promise<{ id: string }> } ) { const { id: paramId } = await context.params; - const userId = req.headers.get('x-user-id'); + const authUser = await getAuthUser(req); - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }); } + const userId = authUser.id; + try { const { value, comment } = await req.json(); @@ -27,7 +30,7 @@ export async function POST( { slug: paramId } ] }, - select: { id: true } + select: { id: true, ownerId: true } }); if (!business) { @@ -35,7 +38,7 @@ export async function POST( } const businessId = business.id; - const ownerId = (business as any).ownerId; + const ownerId = business.ownerId; // 2. Security: Don't allow owner to rate themselves if (userId === ownerId) { @@ -85,7 +88,7 @@ export async function POST( // 3. Aggregate all ratings for this business const stats = await prisma.rating.aggregate({ - where: { businessId }, + where: { businessId, status: 'APPROVED' }, _avg: { value: true }, _count: { value: true } }); diff --git a/app/api/businesses/[id]/rating/me/route.ts b/app/api/businesses/[id]/rating/me/route.ts index 9d1bd43..68e305c 100644 --- a/app/api/businesses/[id]/rating/me/route.ts +++ b/app/api/businesses/[id]/rating/me/route.ts @@ -1,17 +1,20 @@ import { NextRequest, NextResponse } from 'next/server'; import prisma from '@/lib/prisma'; +import { getAuthUser } from '@/lib/auth-utils'; export async function GET( req: NextRequest, context: { params: Promise<{ id: string }> } ) { const { id: paramId } = await context.params; - const userId = req.headers.get('x-user-id'); + const authUser = await getAuthUser(req); - if (!userId) { + if (!authUser) { return NextResponse.json({ rating: null }); } + const userId = authUser.id; + try { // Resolve actual Business UUID if paramId is a slug const business = await prisma.business.findFirst({ diff --git a/app/api/businesses/[id]/route.ts b/app/api/businesses/[id]/route.ts index 44ae472..26e729e 100644 --- a/app/api/businesses/[id]/route.ts +++ b/app/api/businesses/[id]/route.ts @@ -2,8 +2,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' import { USER_SAFE_SELECT, getAuthUser } from '@/lib/auth-utils' -type Params = { params: Promise<{ id: string }> } - // GET /api/businesses/[id] export async function GET( request: NextRequest, @@ -50,11 +48,56 @@ export async function PATCH( context: { params: Promise<{ id: string }> } ): Promise { try { + const authUser = await getAuthUser(request) + if (!authUser) { + return NextResponse.json({ error: 'Authentification requise' }, { status: 401 }) + } + const { id } = await context.params + const existing = await prisma.business.findUnique({ + where: { id }, + select: { ownerId: true } + }) + + if (!existing) { + return NextResponse.json({ error: 'Boutique non trouvée' }, { status: 404 }) + } + + const isAdmin = authUser.role === 'ADMIN' + const isOwner = existing.ownerId === authUser.id + + if (!isOwner && !isAdmin) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 403 }) + } + const body = await request.json() + + // Whitelist allowed fields for owners; sensitive fields require ADMIN role + const allowedOwnerFields = [ + 'name', 'slug', 'category', 'categoryId', 'suggestedCategory', + 'location', 'countryId', 'city', 'description', 'logoUrl', + 'coverUrl', 'coverPosition', 'coverZoom', 'videoUrl', + 'contactEmail', 'contactPhone', 'websiteUrl', 'socialLinks', + 'showEmail', 'showPhone', 'showSocials', 'tags', + 'founderName', 'founderImageUrl', 'keyMetric', 'isActive' + ] + const allowedAdminFields = [ + ...allowedOwnerFields, + 'verified', 'plan', 'isFeatured', 'isHomeFeatured', + 'isSuspended', 'suspensionReason', 'metaTitle', 'metaDescription' + ] + + const permittedKeys = isAdmin ? allowedAdminFields : allowedOwnerFields + const updateData: Record = {} + for (const key of permittedKeys) { + if (body[key] !== undefined) { + updateData[key] = body[key] + } + } + const business = await prisma.business.update({ where: { id }, - data: body, + data: updateData, include: { owner: { select: USER_SAFE_SELECT @@ -75,8 +118,12 @@ export async function DELETE( context: { params: Promise<{ id: string }> } ): Promise { try { + const authUser = await getAuthUser(request) + if (!authUser) { + return NextResponse.json({ error: 'Authentification requise' }, { status: 401 }) + } + const { id } = await context.params - const headerUserId = request.headers.get('x-user-id') // 1. Find business to check owner const business = await prisma.business.findUnique({ @@ -89,9 +136,8 @@ export async function DELETE( } // 2. Auth check (Owner or Admin) - const user = headerUserId ? await prisma.user.findUnique({ where: { id: headerUserId } }) : null - const isAdmin = user?.role === 'ADMIN' - const isOwner = business.ownerId === headerUserId + const isAdmin = authUser.role === 'ADMIN' + const isOwner = business.ownerId === authUser.id if (!isOwner && !isAdmin) { return NextResponse.json({ error: 'Non autorisé' }, { status: 403 }) @@ -101,13 +147,13 @@ export async function DELETE( await prisma.business.delete({ where: { id } }) // 4. Update user role if they don't have other businesses - if (headerUserId && !isAdmin) { + if (!isAdmin) { const otherBusinesses = await prisma.business.findFirst({ - where: { ownerId: headerUserId } + where: { ownerId: authUser.id } }) if (!otherBusinesses) { await prisma.user.update({ - where: { id: headerUserId }, + where: { id: authUser.id }, data: { role: 'VISITOR' } }) } diff --git a/app/api/businesses/me/route.ts b/app/api/businesses/me/route.ts index 53c2eac..b5765f8 100644 --- a/app/api/businesses/me/route.ts +++ b/app/api/businesses/me/route.ts @@ -1,17 +1,18 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' +import { getAuthUser } from '@/lib/auth-utils' // GET /api/businesses/me — Fetch business for the current user export async function GET(request: NextRequest) { try { - const userId = request.headers.get('x-user-id') + const authUser = await getAuthUser(request) - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) } const business = await prisma.business.findFirst({ - where: { ownerId: userId }, + where: { ownerId: authUser.id }, include: { offers: true } }) diff --git a/app/api/businesses/route.ts b/app/api/businesses/route.ts index 533d306..2399248 100644 --- a/app/api/businesses/route.ts +++ b/app/api/businesses/route.ts @@ -91,16 +91,15 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { try { - const body = await request.json() - const headerUserId = request.headers.get('x-user-id') - const { ownerId: bodyOwnerId, ...data } = body - - const ownerId = bodyOwnerId || headerUserId - - if (!ownerId) { - return NextResponse.json({ error: 'ownerId est requis (via body ou x-user-id header)' }, { status: 400 }) + const authUser = await getAuthUser(request) + if (!authUser) { + return NextResponse.json({ error: 'Authentification requise' }, { status: 401 }) } + const body = await request.json() + const { ownerId: _ignoredOwnerId, ...data } = body + const ownerId = authUser.id + // 1. Sanitize & Validate data const cleanData: any = { name: data.name || "", @@ -124,11 +123,7 @@ export async function POST(request: NextRequest) { showEmail: data.showEmail ?? true, showPhone: data.showPhone ?? true, showSocials: data.showSocials ?? true, - verified: data.verified ?? false, - viewCount: data.viewCount ?? 0, - rating: data.rating ?? 0, tags: Array.isArray(data.tags) ? data.tags : [], - isFeatured: data.isFeatured ?? false, founderName: data.founderName || null, founderImageUrl: data.founderImageUrl || null, keyMetric: data.keyMetric || null, @@ -190,11 +185,13 @@ export async function POST(request: NextRequest) { } }); - // Automatic role upgrade - await prisma.user.update({ - where: { id: ownerId }, - data: { role: 'ENTREPRENEUR' } - }); + // Automatic role upgrade (only if VISITOR, never downgrade ADMIN) + if (authUser.role === 'VISITOR') { + await prisma.user.update({ + where: { id: ownerId }, + data: { role: 'ENTREPRENEUR' } + }); + } } console.log('Business saved successfully:', business.id); diff --git a/app/api/conversations/[id]/archive/route.ts b/app/api/conversations/[id]/archive/route.ts index 3459791..b8ac0f2 100644 --- a/app/api/conversations/[id]/archive/route.ts +++ b/app/api/conversations/[id]/archive/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '../../../../../lib/prisma' +import { getAuthUser } from '@/lib/auth-utils' // PATCH /api/conversations/[id]/archive — Toggle archive status for the user export async function PATCH( @@ -7,18 +8,18 @@ export async function PATCH( context: { params: Promise<{ id: string }> } ): Promise { try { - const userId = request.headers.get('x-user-id') + const authUser = await getAuthUser(request) const { id } = await context.params const { archived } = await request.json() - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) } const updatedParticipant = await prisma.conversationParticipant.update({ where: { userId_conversationId: { - userId: userId, + userId: authUser.id, conversationId: id } }, diff --git a/app/api/conversations/[id]/route.ts b/app/api/conversations/[id]/route.ts index bcb281a..b6d7c02 100644 --- a/app/api/conversations/[id]/route.ts +++ b/app/api/conversations/[id]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '../../../../lib/prisma' +import { getAuthUser } from '@/lib/auth-utils' // GET /api/conversations/[id] — Get messages for a conversation export async function GET( @@ -7,13 +8,15 @@ export async function GET( context: { params: Promise<{ id: string }> } ): Promise { try { - const userId = request.headers.get('x-user-id') + const authUser = await getAuthUser(request) const { id } = await context.params - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) } + const userId = authUser.id + // Verify user is a participant const isParticipant = await prisma.conversationParticipant.findUnique({ where: { diff --git a/app/api/conversations/route.ts b/app/api/conversations/route.ts index 675e58f..d478eed 100644 --- a/app/api/conversations/route.ts +++ b/app/api/conversations/route.ts @@ -1,15 +1,17 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' +import { getAuthUser } from '@/lib/auth-utils' // GET /api/conversations — List all conversations for the user export async function GET(request: NextRequest) { try { - const userId = request.headers.get('x-user-id') + const authUser = await getAuthUser(request) - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) } + const userId = authUser.id const { searchParams } = new URL(request.url) const businessId = searchParams.get('businessId') diff --git a/app/api/favorites/route.ts b/app/api/favorites/route.ts index 8350150..4630395 100644 --- a/app/api/favorites/route.ts +++ b/app/api/favorites/route.ts @@ -1,16 +1,17 @@ import { NextRequest, NextResponse } from 'next/server'; import prisma from '@/lib/prisma'; +import { getAuthUser } from '@/lib/auth-utils'; // GET /api/favorites - Get user's favorites export async function GET(request: NextRequest) { try { - const userId = request.headers.get('x-user-id'); - if (!userId) { + const authUser = await getAuthUser(request); + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }); } const favorites = await prisma.favorite.findMany({ - where: { userId }, + where: { userId: authUser.id }, include: { business: { include: { @@ -32,17 +33,19 @@ export async function GET(request: NextRequest) { // POST /api/favorites - Toggle favorite export async function POST(request: NextRequest) { try { - const userId = request.headers.get('x-user-id'); + const authUser = await getAuthUser(request); const body = await request.json(); const { businessId } = body; - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }); } if (!businessId) { return NextResponse.json({ error: 'businessId requis' }, { status: 400 }); } + const userId = authUser.id; + // Resolve business ID (in case slug was passed) const business = await prisma.business.findFirst({ where: { diff --git a/app/api/gemini/route.ts b/app/api/gemini/route.ts index 6a15a40..2f671e6 100644 --- a/app/api/gemini/route.ts +++ b/app/api/gemini/route.ts @@ -1,9 +1,43 @@ import { NextRequest, NextResponse } from 'next/server' import { GoogleGenAI } from '@google/genai' +import { getAuthUser } from '@/lib/auth-utils' + +const RATE_LIMIT_WINDOW_MS = 60 * 1000 // 1 minute +const MAX_REQUESTS_PER_WINDOW = 10 +const rateLimitMap = new Map() + +function checkRateLimit(key: string): boolean { + const now = Date.now() + const entry = rateLimitMap.get(key) + if (!entry || now > entry.resetAt) { + rateLimitMap.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS }) + return true + } + if (entry.count >= MAX_REQUESTS_PER_WINDOW) { + return false + } + entry.count += 1 + return true +} // POST /api/gemini — AI content generation export async function POST(request: NextRequest) { try { + const authUser = await getAuthUser(request) + if (!authUser) { + return NextResponse.json( + { error: 'Authentification requise' }, + { status: 401 } + ) + } + + if (!checkRateLimit(authUser.id)) { + return NextResponse.json( + { error: 'Trop de requêtes IA. Veuillez patienter une minute.' }, + { status: 429 } + ) + } + const apiKey = process.env.GEMINI_API_KEY if (!apiKey || apiKey === 'PLACEHOLDER_API_KEY') { return NextResponse.json( @@ -14,7 +48,10 @@ export async function POST(request: NextRequest) { const ai = new GoogleGenAI({ apiKey }) const body = await request.json() - const { action, name, category, keywords } = body + const action = typeof body.action === 'string' ? body.action : '' + const name = typeof body.name === 'string' ? body.name.trim().slice(0, 100) : '' + const category = typeof body.category === 'string' ? body.category.trim().slice(0, 100) : '' + const keywords = typeof body.keywords === 'string' ? body.keywords.trim().slice(0, 300) : '' if (action === 'description') { const prompt = ` diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 6e3148e..3ec01eb 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -1,15 +1,5 @@ import { NextResponse } from 'next/server'; -export const dynamic = 'force-dynamic'; - export async function GET() { - return NextResponse.json( - { - status: 'ok', - service: 'afrohub-front', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - }, - { status: 200 } - ); + return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() }); } diff --git a/app/api/interviews/[id]/route.ts b/app/api/interviews/[id]/route.ts index 2a59971..ac64ec4 100644 --- a/app/api/interviews/[id]/route.ts +++ b/app/api/interviews/[id]/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '../../../../lib/prisma' - -type Params = { params: Promise<{ id: string }> } +import { isAdmin } from '@/lib/auth-utils' // GET /api/interviews/[id] export async function GET( @@ -19,12 +18,16 @@ export async function GET( } } -// PATCH /api/interviews/[id] +// PATCH /api/interviews/[id] (Admin only) export async function PATCH( request: NextRequest, context: { params: Promise<{ id: string }> } ): Promise { try { + if (!(await isAdmin(request))) { + return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 }) + } + const { id } = await context.params const body = await request.json() const interview = await prisma.interview.update({ where: { id }, data: body }) @@ -35,12 +38,16 @@ export async function PATCH( } } -// DELETE /api/interviews/[id] +// DELETE /api/interviews/[id] (Admin only) export async function DELETE( - _request: NextRequest, + request: NextRequest, context: { params: Promise<{ id: string }> } ): Promise { try { + if (!(await isAdmin(request))) { + return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 }) + } + const { id } = await context.params await prisma.interview.delete({ where: { id } }) return NextResponse.json({ success: true }) diff --git a/app/api/interviews/route.ts b/app/api/interviews/route.ts index 39ca58f..0fde7bd 100644 --- a/app/api/interviews/route.ts +++ b/app/api/interviews/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' +import { isAdmin } from '@/lib/auth-utils' // GET /api/interviews export async function GET() { @@ -20,9 +21,13 @@ export async function GET() { } } -// POST /api/interviews +// POST /api/interviews (Admin only) export async function POST(request: NextRequest) { try { + if (!(await isAdmin(request))) { + return NextResponse.json({ error: 'Accès réservé aux administrateurs' }, { status: 403 }) + } + const body = await request.json() const interview = await prisma.interview.create({ data: body }) return NextResponse.json(interview, { status: 201 }) diff --git a/app/api/messages/[id]/report/route.ts b/app/api/messages/[id]/report/route.ts index 08fd2fd..d56ee11 100644 --- a/app/api/messages/[id]/report/route.ts +++ b/app/api/messages/[id]/report/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '../../../../../lib/prisma' +import { getAuthUser } from '@/lib/auth-utils' // POST /api/messages/[id]/report — Report a message for moderation export async function POST( @@ -7,14 +8,16 @@ export async function POST( context: { params: Promise<{ id: string }> } ): Promise { try { - const userId = request.headers.get('x-user-id') + const authUser = await getAuthUser(request) const { id } = await context.params const { reason } = await request.json() - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) } + const userId = authUser.id + // Verify message exists const message = await prisma.message.findUnique({ where: { id } diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts index d656d80..19c277b 100644 --- a/app/api/messages/route.ts +++ b/app/api/messages/route.ts @@ -1,16 +1,19 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' +import { getAuthUser } from '@/lib/auth-utils' // POST /api/messages — Send a message or start a conversation export async function POST(request: NextRequest) { try { - const userId = request.headers.get('x-user-id') + const authUser = await getAuthUser(request) const { businessId, conversationId, content } = await request.json() - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) } + const userId = authUser.id + if (!content) { return NextResponse.json({ error: 'Contenu manquant' }, { status: 400 }) } diff --git a/app/api/messages/unread/route.ts b/app/api/messages/unread/route.ts index ad9e267..1e8bcfe 100644 --- a/app/api/messages/unread/route.ts +++ b/app/api/messages/unread/route.ts @@ -1,14 +1,17 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' +import { getAuthUser } from '@/lib/auth-utils' export async function GET(request: NextRequest) { try { - const userId = request.headers.get('x-user-id') + const authUser = await getAuthUser(request) - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) } + const userId = authUser.id + const unreadCount = await prisma.message.count({ where: { conversation: { diff --git a/app/api/offers/[id]/route.ts b/app/api/offers/[id]/route.ts index 5fdb075..eb8d757 100644 --- a/app/api/offers/[id]/route.ts +++ b/app/api/offers/[id]/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '../../../../lib/prisma' - -type Params = { params: Promise<{ id: string }> } +import { getAuthUser } from '@/lib/auth-utils' // GET /api/offers/[id] export async function GET( @@ -22,17 +21,43 @@ export async function GET( } } -// PATCH /api/offers/[id] +// PATCH /api/offers/[id] (Owner or Admin only) export async function PATCH( request: NextRequest, context: { params: Promise<{ id: string }> } ): Promise { try { + const authUser = await getAuthUser(request) + if (!authUser) { + return NextResponse.json({ error: 'Authentification requise' }, { status: 401 }) + } + const { id } = await context.params + const existing = await prisma.offer.findUnique({ + where: { id }, + include: { business: { select: { ownerId: true } } } + }) + + if (!existing) { + return NextResponse.json({ error: 'Offre non trouvée' }, { status: 404 }) + } + + if (existing.business.ownerId !== authUser.id && authUser.role !== 'ADMIN') { + return NextResponse.json({ error: 'Non autorisé' }, { status: 403 }) + } + const body = await request.json() + const { title, description, price, type, imageUrl } = body + const offer = await prisma.offer.update({ where: { id }, - data: body, + data: { + ...(title !== undefined ? { title } : {}), + ...(description !== undefined ? { description } : {}), + ...(price !== undefined ? { price } : {}), + ...(type !== undefined ? { type } : {}), + ...(imageUrl !== undefined ? { imageUrl } : {}), + }, include: { business: true }, }) return NextResponse.json(offer) @@ -42,13 +67,31 @@ export async function PATCH( } } -// DELETE /api/offers/[id] +// DELETE /api/offers/[id] (Owner or Admin only) export async function DELETE( - _request: NextRequest, + request: NextRequest, context: { params: Promise<{ id: string }> } ): Promise { try { + const authUser = await getAuthUser(request) + if (!authUser) { + return NextResponse.json({ error: 'Authentification requise' }, { status: 401 }) + } + const { id } = await context.params + const existing = await prisma.offer.findUnique({ + where: { id }, + include: { business: { select: { ownerId: true } } } + }) + + if (!existing) { + return NextResponse.json({ error: 'Offre non trouvée' }, { status: 404 }) + } + + if (existing.business.ownerId !== authUser.id && authUser.role !== 'ADMIN') { + return NextResponse.json({ error: 'Non autorisé' }, { status: 403 }) + } + await prisma.offer.delete({ where: { id } }) return NextResponse.json({ success: true }) } catch (error) { diff --git a/app/api/offers/route.ts b/app/api/offers/route.ts index 003196c..3890b35 100644 --- a/app/api/offers/route.ts +++ b/app/api/offers/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' +import { getAuthUser } from '@/lib/auth-utils' // GET /api/offers — optionally filter by businessId export async function GET(request: NextRequest) { @@ -22,11 +23,16 @@ export async function GET(request: NextRequest) { } } -// POST /api/offers +// POST /api/offers (Owner or Admin only) export async function POST(request: NextRequest) { try { + const authUser = await getAuthUser(request) + if (!authUser) { + return NextResponse.json({ error: 'Authentification requise' }, { status: 401 }) + } + const body = await request.json() - const { businessId } = body + const { businessId, title, description, price, type, imageUrl } = body if (!businessId) { return NextResponse.json({ error: 'businessId manquant' }, { status: 400 }) @@ -44,6 +50,10 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Entreprise non trouvée' }, { status: 404 }) } + if (business.ownerId !== authUser.id && authUser.role !== 'ADMIN') { + return NextResponse.json({ error: 'Non autorisé : vous n\'êtes pas propriétaire de cette entreprise' }, { status: 403 }) + } + // Fetch the specific limit for this tier from the database const planDetails = await prisma.pricingPlan.findUnique({ where: { tier: business.plan } @@ -63,9 +73,16 @@ export async function POST(request: NextRequest) { }, { status: 403 }) } - // 3. Create offer + // 3. Create offer with whitelisted fields const offer = await prisma.offer.create({ - data: body, + data: { + businessId, + title: title || '', + description: description || '', + price: price ?? '', + type: type || 'PRODUCT', + imageUrl: imageUrl || '', + }, include: { business: true }, }) return NextResponse.json(offer, { status: 201 }) diff --git a/app/api/ratings/[id]/reply/route.ts b/app/api/ratings/[id]/reply/route.ts index 0df3e5e..f5653e7 100644 --- a/app/api/ratings/[id]/reply/route.ts +++ b/app/api/ratings/[id]/reply/route.ts @@ -1,17 +1,20 @@ import { NextRequest, NextResponse } from 'next/server'; import prisma from '@/lib/prisma'; +import { getAuthUser } from '@/lib/auth-utils'; export async function POST( req: NextRequest, context: { params: Promise<{ id: string }> } ) { const { id: ratingId } = await context.params; - const userId = req.headers.get('x-user-id'); + const authUser = await getAuthUser(req); - if (!userId) { + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }); } + const userId = authUser.id; + try { const { reply } = await req.json(); diff --git a/app/api/users/me/route.ts b/app/api/users/me/route.ts index 01257df..48c1872 100644 --- a/app/api/users/me/route.ts +++ b/app/api/users/me/route.ts @@ -1,37 +1,52 @@ import { NextRequest, NextResponse } from 'next/server' import prisma from '@/lib/prisma' +import { getAuthUser, USER_SAFE_SELECT } from '@/lib/auth-utils' -// PATCH /api/users/me — Update personal user profile -export async function PATCH( - request: NextRequest, - context: { params: Promise } -): Promise { +// GET /api/users/me — Get current authenticated user profile +export async function GET(request: NextRequest): Promise { try { - const userId = request.headers.get('x-user-id') - const data = await request.json() - - if (!userId) { + const authUser = await getAuthUser(request) + if (!authUser) { return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) } + const user = await prisma.user.findUnique({ + where: { id: authUser.id }, + select: USER_SAFE_SELECT + }) + + if (!user) { + return NextResponse.json({ error: 'Utilisateur introuvable' }, { status: 404 }) + } + + return NextResponse.json(user) + } catch (error) { + console.error('GET /api/users/me error:', error) + return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 }) + } +} + +// PATCH /api/users/me — Update personal user profile +export async function PATCH( + request: NextRequest +): Promise { + try { + const authUser = await getAuthUser(request) + if (!authUser) { + return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 }) + } + + const data = await request.json() + const updatedUser = await prisma.user.update({ - where: { id: userId }, + where: { id: authUser.id }, data: { name: data.name, phone: data.phone, bio: data.bio, location: data.location, }, - select: { - id: true, - name: true, - email: true, - role: true, - phone: true, - bio: true, - location: true, - avatar: true - } + select: USER_SAFE_SELECT }) return NextResponse.json(updatedUser) diff --git a/app/api/users/profile/route.ts b/app/api/users/profile/route.ts index eed5467..7c6bf51 100644 --- a/app/api/users/profile/route.ts +++ b/app/api/users/profile/route.ts @@ -1,27 +1,25 @@ import { NextRequest, NextResponse } from 'next/server'; import prisma from '@/lib/prisma'; +import { getAuthUser, USER_SAFE_SELECT } from '@/lib/auth-utils'; export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const email = searchParams.get('email'); + const authUser = await getAuthUser(request); + if (!authUser) { + return NextResponse.json({ error: 'Authentification requise' }, { status: 401 }); + } - if (!email) { - return NextResponse.json({ error: 'Email requis' }, { status: 400 }); + const { searchParams } = new URL(request.url); + const email = searchParams.get('email')?.toLowerCase().trim() || authUser.email.toLowerCase(); + + // Seul l'utilisateur lui-même ou un administrateur peut consulter ce profil par email + if (authUser.role !== 'ADMIN' && authUser.email.toLowerCase() !== email) { + return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 }); } try { const user = await prisma.user.findUnique({ where: { email }, - select: { - id: true, - name: true, - email: true, - role: true, - avatar: true, - phone: true, - bio: true, - location: true - } + select: USER_SAFE_SELECT }); if (!user) { diff --git a/app/api/users/route.ts b/app/api/users/route.ts index 8f525e8..4132353 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -1,8 +1,9 @@ import { NextRequest, NextResponse } from 'next/server' +import bcrypt from 'bcryptjs' import prisma from '@/lib/prisma' import { isAdmin, USER_SAFE_SELECT } from '@/lib/auth-utils' -// GET /api/users +// GET /api/users (Admin only) export async function GET(request: NextRequest) { try { if (!(await isAdmin(request))) { @@ -23,7 +24,7 @@ export async function GET(request: NextRequest) { } } -// POST /api/users (Admin only) +// POST /api/users (Admin only — hashes password and whitelists fields) export async function POST(request: NextRequest) { try { if (!(await isAdmin(request))) { @@ -31,12 +32,33 @@ export async function POST(request: NextRequest) { } const body = await request.json() - - // Note: For admins, we might want to allow creating users directly, - // but we should still ensure password hashing if provided. - // For now, we just restrict it to admins to prevent the public leak. + const email = body.email?.toLowerCase().trim() + const name = body.name?.trim() + const rawPassword = body.password + + if (!email || !name || !rawPassword || rawPassword.length < 8) { + return NextResponse.json( + { error: 'Nom, email et mot de passe (min. 8 caractères) requis' }, + { status: 400 } + ) + } + + const hashedPassword = await bcrypt.hash(rawPassword, 10) + const allowedRole = ['VISITOR', 'ENTREPRENEUR', 'ADMIN'].includes(body.role) + ? body.role + : 'VISITOR' + const user = await prisma.user.create({ - data: body, + data: { + name, + email, + password: hashedPassword, + role: allowedRole, + phone: body.phone || null, + bio: body.bio || null, + location: body.location || null, + emailVerified: true, + }, select: USER_SAFE_SELECT }) return NextResponse.json(user, { status: 201 }) diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 5106433..c243ceb 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -46,9 +46,7 @@ const DashboardContent = () => { const fetchBusiness = async () => { try { - const res = await fetch('/api/businesses/me', { - headers: { 'x-user-id': user.id } - }); + const res = await fetch('/api/businesses/me'); const data = await res.json(); if (data && !data.error) { setBusiness(data); @@ -77,26 +75,9 @@ const DashboardContent = () => { // Fetch unread messages count const fetchUnreadCount = async () => { if (!user || !isMounted) return; - - // If user has no id, try to find it via email (Session Reconciliation) - let effectiveUserId = user.id; - if (!effectiveUserId || effectiveUserId === 'undefined' || effectiveUserId === 'null') { - try { - const res = await fetch(`/api/users/profile?email=${encodeURIComponent(user.email)}`); - const data = await res.json(); - if (data.id) { - effectiveUserId = data.id; - // Note: Ideally we'd update the global user object here too - } else { - return; // Can't count without ID - } - } catch (e) { return; } - } try { - const res = await fetch('/api/messages/unread', { - headers: { 'x-user-id': effectiveUserId } - }); + const res = await fetch('/api/messages/unread'); const data = await res.json(); if (!data.error) { setUnreadCount(data.count); diff --git a/components/Footer.tsx b/components/Footer.tsx index 5bbedba..5bf5fe6 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -5,6 +5,7 @@ import React from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Facebook, Twitter, Instagram, Linkedin } from 'lucide-react'; +import { safeUrl } from '@/lib/utils'; interface FooterProps { settings?: any; @@ -49,7 +50,7 @@ const Footer = ({ settings }: FooterProps) => { {socialLinks.map((social, index) => ( {
    {slide.type === 'BUSINESS' ? 'Voir la fiche' : 'En savoir plus'} diff --git a/components/UserProvider.tsx b/components/UserProvider.tsx index 5462838..b0df2c6 100644 --- a/components/UserProvider.tsx +++ b/components/UserProvider.tsx @@ -22,9 +22,27 @@ export function UserProvider({ children }: { children: ReactNode }) { React.useEffect(() => { const savedUser = localStorage.getItem('afro_user'); if (savedUser) { - setUser(JSON.parse(savedUser)); + try { + setUser(JSON.parse(savedUser)); + } catch { + localStorage.removeItem('afro_user'); + } } + // Synchronize user session from signed HttpOnly cookie + fetch('/api/users/me') + .then(async res => { + if (res.ok) { + const freshUser = await res.json(); + setUser(freshUser); + localStorage.setItem('afro_user', JSON.stringify(freshUser)); + } else if (res.status === 401) { + setUser(null); + localStorage.removeItem('afro_user'); + } + }) + .catch(err => console.error('Error verifying session:', err)); + // Fetch settings fetch('/api/settings') .then(res => res.json()) @@ -47,6 +65,9 @@ export function UserProvider({ children }: { children: ReactNode }) { const logout = () => { setUser(null); localStorage.removeItem('afro_user'); + fetch('/api/auth/logout', { method: 'POST' }).catch(err => + console.error('Error logging out:', err) + ); }; return ( diff --git a/lib/auth-utils.ts b/lib/auth-utils.ts index d166c36..90318ac 100644 --- a/lib/auth-utils.ts +++ b/lib/auth-utils.ts @@ -1,6 +1,18 @@ import { NextRequest } from 'next/server'; +import { cookies } from 'next/headers'; import prisma from '@/lib/prisma'; +export const USER_SESSION_COOKIE_NAME = 'afro_session'; +export const DEFAULT_USER_SESSION_DURATION = 7 * 24 * 60 * 60; // 7 days in seconds + +export interface UserSessionPayload { + userId: string; + email: string; + role: string; + exp: number; + iat: number; +} + export const USER_SAFE_SELECT = { id: true, name: true, @@ -10,25 +22,163 @@ export const USER_SAFE_SELECT = { bio: true, location: true, phone: true, + countryId: true, + isSuspended: true, + suspensionReason: true, createdAt: true, updatedAt: true, emailVerified: true, }; -export async function getAuthUser(request: NextRequest) { - const userId = request.headers.get('x-user-id'); - if (!userId) return null; +function getJwtSecret(): string { + const secret = process.env.JWT_SECRET || process.env.NEXTAUTH_SECRET || process.env.ADMIN_JWT_SECRET; + if (!secret) { + if (process.env.NODE_ENV === 'production') { + throw new Error('CRITICAL: JWT_SECRET or NEXTAUTH_SECRET environment variable must be set in production.'); + } + return 'dev_only_afrohub_user_jwt_secret_key_change_in_prod'; + } + return secret; +} + +function base64UrlEncode(str: string): string { + return Buffer.from(str, 'utf-8').toString('base64url'); +} + +function base64UrlDecode(str: string): string { + return Buffer.from(str, 'base64url').toString('utf-8'); +} + +async function getCryptoKey(): Promise { + const enc = new TextEncoder(); + return await crypto.subtle.importKey( + 'raw', + enc.encode(getJwtSecret()), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign', 'verify'] + ); +} + +export async function signUserToken( + payload: Omit, + expiresInSeconds: number = DEFAULT_USER_SESSION_DURATION +): Promise { + const now = Math.floor(Date.now() / 1000); + const fullPayload: UserSessionPayload = { + ...payload, + iat: now, + exp: now + expiresInSeconds, + }; + + const header = { alg: 'HS256', typ: 'JWT' }; + const headerB64 = base64UrlEncode(JSON.stringify(header)); + const payloadB64 = base64UrlEncode(JSON.stringify(fullPayload)); + const dataToSign = `${headerB64}.${payloadB64}`; + + const key = await getCryptoKey(); + const signatureBuffer = await crypto.subtle.sign( + 'HMAC', + key, + new TextEncoder().encode(dataToSign) + ); + + const signatureB64 = Buffer.from(signatureBuffer).toString('base64url'); + return `${dataToSign}.${signatureB64}`; +} + +export async function verifyUserToken(token?: string | null): Promise { + if (!token || typeof token !== 'string') return null; + const parts = token.split('.'); + if (parts.length !== 3) return null; + + const [headerB64, payloadB64, signatureB64] = parts; + const dataToSign = `${headerB64}.${payloadB64}`; try { + const key = await getCryptoKey(); + const signatureBytes = Buffer.from(signatureB64, 'base64url'); + + const isValid = await crypto.subtle.verify( + 'HMAC', + key, + signatureBytes, + new TextEncoder().encode(dataToSign) + ); + + if (!isValid) return null; + + const payloadJson = base64UrlDecode(payloadB64); + const payload = JSON.parse(payloadJson) as UserSessionPayload; + + const now = Math.floor(Date.now() / 1000); + if (!payload.exp || payload.exp < now || !payload.userId) { + return null; + } + + return payload; + } catch { + return null; + } +} + +export async function setUserSessionCookie( + token: string, + maxAgeSeconds: number = DEFAULT_USER_SESSION_DURATION +): Promise { + const cookieStore = await cookies(); + cookieStore.set(USER_SESSION_COOKIE_NAME, token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: maxAgeSeconds, + }); +} + +export async function clearUserSessionCookie(): Promise { + const cookieStore = await cookies(); + cookieStore.set(USER_SESSION_COOKIE_NAME, '', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + maxAge: 0, + }); +} + +export async function getAuthUser(request?: NextRequest) { + try { + let token: string | undefined; + + if (request) { + token = request.cookies.get(USER_SESSION_COOKIE_NAME)?.value; + } + if (!token) { + const cookieStore = await cookies(); + token = cookieStore.get(USER_SESSION_COOKIE_NAME)?.value; + } + + const payload = await verifyUserToken(token); + if (!payload?.userId) return null; + const user = await prisma.user.findUnique({ - where: { id: userId }, + where: { id: payload.userId }, select: { id: true, + email: true, + name: true, role: true, emailVerified: true, - isSuspended: true + isSuspended: true, + deletedAt: true, } }); + + if (!user || user.deletedAt) { + return null; + } + return user; } catch (error) { console.error('Error fetching auth user:', error); @@ -36,7 +186,7 @@ export async function getAuthUser(request: NextRequest) { } } -export async function isAdmin(request: NextRequest) { +export async function isAdmin(request?: NextRequest) { const user = await getAuthUser(request); - return user?.role === 'ADMIN'; + return user?.role === 'ADMIN' && !user.isSuspended; } diff --git a/lib/geo.ts b/lib/geo.ts index 277e677..65c7b64 100644 --- a/lib/geo.ts +++ b/lib/geo.ts @@ -1,3 +1,5 @@ +import { isIP } from 'node:net'; + export async function getGeoInfo(ip: string, headers: Headers) { // 1. Try platform-specific headers first (Vercel, Cloudflare, etc.) const headerCountry = @@ -6,25 +8,33 @@ export async function getGeoInfo(ip: string, headers: Headers) { headers.get('x-country-code'); if (headerCountry && headerCountry !== 'XX') { - // If we have a country code, we might still want to get the name - // For now, let's keep it simple or use a lookup map return { country: headerCountry, city: headers.get('x-vercel-ip-city') || 'Unknown' }; } + const cleanIp = ip.trim(); + // 2. Handle localhost - if (ip === '127.0.0.1' || ip === '::1' || ip.includes('127.0.0.1')) { + if (cleanIp === '127.0.0.1' || cleanIp === '::1' || cleanIp.includes('127.0.0.1')) { return { country: 'Local / Dev', city: 'Localhost' }; } + // Validate IPv4/IPv6 syntax to prevent SSRF / path traversal via spoofed X-Forwarded-For + if (isIP(cleanIp) === 0) { + return { country: 'Unknown', city: 'Unknown' }; + } + // 3. Fallback to external API try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2000); // 2s timeout - const response = await fetch(`http://ip-api.com/json/${ip}`, { signal: controller.signal }); + const response = await fetch(`http://ip-api.com/json/${encodeURIComponent(cleanIp)}`, { + signal: controller.signal, + headers: { 'Accept-Encoding': 'gzip, deflate' }, + }); clearTimeout(timeoutId); if (response.ok) { diff --git a/lib/mail.ts b/lib/mail.ts index 8b5c0ee..330f0d8 100644 --- a/lib/mail.ts +++ b/lib/mail.ts @@ -1,11 +1,5 @@ import nodemailer from 'nodemailer'; -console.log('SMTP Config:', { - host: process.env.SMTP_HOST, - port: process.env.SMTP_PORT, - secure: process.env.SMTP_SECURE, - user: process.env.SMTP_USER -}); const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT || '587'), @@ -15,7 +9,7 @@ const transporter = nodemailer.createTransport({ pass: process.env.SMTP_PASS, }, tls: { - rejectUnauthorized: false + rejectUnauthorized: process.env.NODE_ENV === 'production' } }); @@ -29,7 +23,6 @@ export const sendEmail = async ({ to, subject, html }: { to: string; subject: st try { const info = await transporter.sendMail(mailOptions); - console.log('Email sent: ' + info.response); return info; } catch (error) { console.error('Error sending email:', error); diff --git a/lib/sanitize.ts b/lib/sanitize.ts index 0e71076..a216238 100644 --- a/lib/sanitize.ts +++ b/lib/sanitize.ts @@ -20,10 +20,10 @@ export function sanitizeHTML(html: string): string { allowedAttributes: { 'a': ['href', 'target', 'rel'], 'img': ['src', 'alt'], - '*': ['class', 'style'] + '*': ['class'] }, selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], // Standard URLs only - allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'tel'], + allowedSchemes: ['http', 'https', 'mailto', 'tel'], }); } diff --git a/lib/utils.ts b/lib/utils.ts index 5d55ea0..19d8cf2 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -11,3 +11,28 @@ export function generateSlug(text: string): string { .replace(/^-+/, '') // trim - from start of text .replace(/-+$/, ''); // trim - from end of text } + +/** + * Validates external or relative URLs to prevent javascript:/data:/vbscript: XSS in href attributes. + */ +export function safeUrl(url?: string | null): string { + if (!url || typeof url !== 'string') return '#'; + const trimmed = url.trim(); + if (!trimmed) return '#'; + + // Allow safe relative paths (starting with single `/` and not `//` or `/\`) + if (trimmed.startsWith('/') && !trimmed.startsWith('//') && !trimmed.startsWith('/\\')) { + return trimmed; + } + + try { + const parsed = new URL(trimmed); + if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { + return parsed.toString(); + } + } catch { + // Invalid URL + } + + return '#'; +} diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..a419cbe 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,7 @@ /// /// import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package-lock.json b/package-lock.json index 7d3c5c9..528ef75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,22 +17,21 @@ "@prisma/config": "6.19.3", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.19", - "@types/bcryptjs": "^2.4.6", "@types/node": "^22.14.0", - "@types/pg": "^8.16.0", + "@types/pg": "^8.20.0", "@types/sanitize-html": "^2.16.1", "autoprefixer": "^10.4.27", "bcryptjs": "^3.0.3", - "dotenv": "^17.3.1", + "dotenv": "^17.4.1", "jiti": "^2.6.1", "lucide-react": "^0.554.0", - "next": "16.2.3", - "nodemailer": "^8.0.6", - "pg": "^8.19.0", - "postcss": "^8.5.10", + "next": "^16.4.3", + "nodemailer": "^10.3.1", + "pg": "^8.20.0", + "postcss": "^8.5.23", "prisma": "6.19.3", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", "react-hot-toast": "^2.6.0", "react-is": "^19.2.6", "recharts": "^3.8.1", @@ -46,19 +45,19 @@ "concurrently": "^9.2.1" }, "engines": { - "node": "24.x" + "node": ">=22.0.0" } }, "admin": { "version": "0.1.0", "dependencies": { - "@types/bcryptjs": "^2.4.6", - "afrohub": "file:..", + "@prisma/client": "6.19.3", "bcryptjs": "^3.0.3", "d3-scale": "^4.0.2", "dotenv": "^17.4.1", "lucide-react": "^1.8.0", - "next": "16.2.3", + "next": "^16.4.3", + "nodemailer": "^10.3.1", "pg": "^8.20.0", "react": "19.2.4", "react-dom": "19.2.4", @@ -71,18 +70,30 @@ "devDependencies": { "@tailwindcss/postcss": "^4", "@types/d3-scale": "^4.0.9", - "@types/node": "^20", + "@types/node": "^22.14.0", + "@types/nodemailer": "^8.0.0", "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", "@types/react-simple-maps": "^3.0.6", "@types/topojson-client": "^3.1.5", "eslint": "^9", - "eslint-config-next": "16.2.3", + "eslint-config-next": "^16.4.3", "tailwindcss": "^4", "typescript": "^5" } }, + "admin/node_modules/@next/eslint-plugin-next": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.3.tgz", + "integrity": "sha512-oxuC06wt/71h6ACYCV9GbKOfnf5a7VlRe/p80YHTut15TNgDEmynDnaq8t/xhy5rlvk7dhItIPs6H3rvIM4kyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", + "fast-glob": "3.3.1" + } + }, "admin/node_modules/@types/node": { "version": "20.19.40", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.40.tgz", @@ -93,6 +104,46 @@ "undici-types": "~6.21.0" } }, + "admin/node_modules/eslint-config-next": { + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.3.tgz", + "integrity": "sha512-j9IqBma6abp800frdUURaizYMgXOK+jNGyq6CsYcM9AxpSJs5D/TgMNGeAmRUilZvA7g9V1Tr71gNg9/SVTuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.4.3", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.1.0", + "globals": "16.4.0", + "typescript-eslint": "^8.56.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "admin/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "admin/node_modules/lucide-react": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", @@ -123,6 +174,21 @@ "react": "^19.2.4" } }, + "admin/node_modules/react-quill-new": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.7.0.tgz", + "integrity": "sha512-i5qz1WukIR84ktJrA2VMcZdkRrfOl/4TAK6ygMTX1/9Grcrgj0NW+LnoanBhuIbHaHJZwcvcWMBvSjUFsxQIQA==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21", + "quill": "~2.0.2" + }, + "peerDependencies": { + "quill-delta": "^5.1.0", + "react": "^16 || ^17 || ^18 || ^19", + "react-dom": "^16 || ^17 || ^18 || ^19" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -136,13 +202,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -151,9 +217,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -161,21 +227,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -202,14 +268,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -219,14 +285,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -246,9 +312,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -256,29 +322,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -288,9 +354,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", "dev": true, "license": "MIT", "engines": { @@ -298,9 +364,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -308,9 +374,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -318,27 +384,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.11" }, "bin": { "parser": "bin/babel-parser.js" @@ -348,33 +414,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -382,14 +448,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -407,9 +473,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", "optional": true, "dependencies": { @@ -427,9 +493,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -443,9 +509,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -459,9 +525,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -475,9 +541,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -491,9 +557,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -507,9 +573,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -523,9 +589,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -539,9 +605,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -555,9 +621,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -571,9 +637,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -587,9 +653,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -603,9 +669,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -619,9 +685,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -635,9 +701,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -651,9 +717,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -667,9 +733,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -683,9 +749,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -699,9 +765,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -715,9 +781,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -731,9 +797,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -747,9 +813,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -763,9 +829,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -779,9 +845,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -795,9 +861,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -811,9 +877,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -827,9 +893,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -1086,9 +1152,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -1098,19 +1164,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -1120,19 +1186,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -1146,9 +1231,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -1162,9 +1247,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -1178,9 +1263,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -1194,9 +1279,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -1210,9 +1295,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -1226,9 +1311,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -1242,9 +1327,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -1258,9 +1343,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -1274,9 +1359,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -1290,9 +1375,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -1302,19 +1387,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -1324,19 +1409,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -1346,19 +1431,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -1368,19 +1453,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -1390,19 +1475,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -1412,19 +1497,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -1434,19 +1519,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -1456,38 +1541,54 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@emnapi/runtime": "^1.11.3" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -1497,16 +1598,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -1516,16 +1617,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1535,7 +1636,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1599,25 +1700,15 @@ } }, "node_modules/@next/env": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", - "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.3.tgz", + "integrity": "sha512-a7tz1QCcWWkwC/rPJcPMJk/UNRu9CFhDd3OHYE5hddbJrs8BqRTalxI6GlKCWEGzRLHN+S/B7a4PAHCtm25aBQ==", "license": "MIT" }, - "node_modules/@next/eslint-plugin-next": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.3.tgz", - "integrity": "sha512-nE/b9mht28XJxjTwKs/yk7w4XTaU3t40UHVAky6cjiijdP/SEy3hGsnQMPxmXPTpC7W4/97okm6fngKnvCqVaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "3.3.1" - } - }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", - "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.3.tgz", + "integrity": "sha512-4yq5EXBt+VDtAZwTyfijaFp5D2C5oz3Ikw5a2kiX2K9nw72vrZX2q5UuAlkPtUMO1tQz6JTKpaKWjS7OOefTDQ==", "cpu": [ "arm64" ], @@ -1631,9 +1722,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", - "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.3.tgz", + "integrity": "sha512-1Rvkn/LUhnEF8WxnRkOrqGhbwXui1bkWFxONKIbN+r7C+9KHDSZ+iUoj4CzymVMWUyZIyx9szcb9+kbKhD500A==", "cpu": [ "x64" ], @@ -1647,9 +1738,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", - "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.3.tgz", + "integrity": "sha512-dCKpXErGg3abUM6t5JTJV/ottVBTStJB5lRJTbgdUdQaQ+jNHiK56Hsc0YAEg/rinSh42hYG3HWiYmEMdilnxg==", "cpu": [ "arm64" ], @@ -1663,9 +1754,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", - "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.3.tgz", + "integrity": "sha512-WwYbhWkhYBvBtib1K4W0kL8MW2snBD2A5b3SN4vPpqalZbZs5+bQ1yzAdepVoOqrFfEW3dmREy4Zs1MrWcrFbw==", "cpu": [ "arm64" ], @@ -1679,9 +1770,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", - "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.3.tgz", + "integrity": "sha512-QymWvfUwDEC5/lpJOsxX5VAbyoR2D13+2Jsjz4be7gT4LPqEgdpkeLIR/HFfCLgKzoMhpJbp6I/JnogyNZHcaA==", "cpu": [ "x64" ], @@ -1695,9 +1786,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", - "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.3.tgz", + "integrity": "sha512-Vmjd9D7DpA4Vv3cUlr82QWgJD/BQZmns801vtC5VWHg6jCIA4a5vMKpQnfrM2pyCrp2eV0ENdUg4PlN3bAc2Yw==", "cpu": [ "x64" ], @@ -1711,9 +1802,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", - "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.3.tgz", + "integrity": "sha512-lG1pPdPLk7gTyGb00n5JIMLdBJgjb+UEhSdZvFbhUMg0DyC6z4B7RnpSKF9VCd4imkLObVbXOFA0kFCpw7yFow==", "cpu": [ "arm64" ], @@ -1727,9 +1818,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", - "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.3.tgz", + "integrity": "sha512-NmQC9RkJdnPXRaMxtkCzobB3teVjqF33oSOnwDfi+XYGWWYpt+xlGK3mtktSKLbdjwaWbJu2OkkMiRFGUUncKg==", "cpu": [ "x64" ], @@ -1902,25 +1993,24 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -1929,12 +2019,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -1948,9 +2032,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, "node_modules/@reduxjs/toolkit": { @@ -2009,9 +2093,9 @@ "license": "MIT" }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -2295,12 +2379,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/bcryptjs": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "license": "MIT" - }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -2741,16 +2819,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -3120,10 +3198,6 @@ "resolved": "admin", "link": true }, - "node_modules/afrohub": { - "resolved": "", - "link": true - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3470,9 +3544,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.22", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz", - "integrity": "sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==", + "version": "2.11.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz", + "integrity": "sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -3500,9 +3574,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -3524,9 +3598,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", "funding": [ { "type": "opencollective", @@ -3543,11 +3617,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" @@ -3663,9 +3737,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001790", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", - "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "version": "1.0.30001820", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz", + "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==", "funding": [ { "type": "opencollective", @@ -4062,12 +4136,6 @@ "d3-selection": "2" } }, - "node_modules/d3-transition/node_modules/d3-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", - "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==", - "license": "BSD-3-Clause" - }, "node_modules/d3-transition/node_modules/d3-ease": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz", @@ -4102,12 +4170,6 @@ "d3-transition": "2" } }, - "node_modules/d3-zoom/node_modules/d3-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", - "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==", - "license": "BSD-3-Clause" - }, "node_modules/d3-zoom/node_modules/d3-interpolate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", @@ -4187,6 +4249,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4217,22 +4285,23 @@ "dev": true, "license": "MIT" }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz", + "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], "license": "BSD-3-Clause", "engines": { - "node": ">=16.0.0" + "node": ">=16.9.0" } }, "node_modules/define-data-property": { @@ -4419,9 +4488,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.344", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", - "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "version": "1.5.467", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.467.tgz", + "integrity": "sha512-Nw9X371rtZmYcBSnEijnCDONILjs93biCZO34juC7MBOmK8E+GFnyD4T/WvyrlJkppul7oxg6TwvtKst9SODTw==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -4453,6 +4522,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -4641,9 +4722,9 @@ ] }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -4653,32 +4734,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escalade": { @@ -4762,46 +4843,6 @@ } } }, - "node_modules/eslint-config-next": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.3.tgz", - "integrity": "sha512-Dnkrylzjof/Az7iNoIQJqD18zTxQZcngir19KJaiRsMnnjpQSVoa6aEg/1Q4hQC+cW90uTlgQYadwL1CYNwFWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/eslint-plugin-next": "16.2.3", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^7.0.0", - "globals": "16.4.0", - "typescript-eslint": "^8.46.0" - }, - "peerDependencies": { - "eslint": ">=9.0.0", - "typescript": ">=3.3.1" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-config-next/node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-import-resolver-node": { "version": "0.3.10", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", @@ -6178,9 +6219,9 @@ } }, "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.1.0.tgz", + "integrity": "sha512-bUi/yjmtKYcRVUtWRGr0UA6xEFh2I6zWUwMrUXB3s7bmYCaZ8a+0ZsTRkrawh/mzlSD1Y0Ph8bp/U+TvBpWDNw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6379,10 +6420,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6514,6 +6565,15 @@ "node": ">=0.10" } }, + "node_modules/launder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/launder/-/launder-1.7.2.tgz", + "integrity": "sha512-DLg3HPnHUfBi5/MxMLmJD12dmlMpFEC2HgMW6vkZ/9JR0RU00kXoRGvDxAvFmdO0o610OA77i4FgNTLucmhDVg==", + "license": "MIT", + "dependencies": { + "dayjs": "^1.11.23" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6929,9 +6989,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -6970,16 +7030,16 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", - "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", + "version": "16.4.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.3.tgz", + "integrity": "sha512-OqxKTGi9u80FydydsMNYNjnuje9Au+kvHlt2GZ//ftdpDHm1dxKqAaENoOOTX5wKzn+9vaKUNIfZeyZUuLhCnA==", "license": "MIT", "dependencies": { - "@next/env": "16.2.3", - "@swc/helpers": "0.5.15", + "@next/env": "16.4.3", + "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", + "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "bin": { @@ -6989,15 +7049,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.3", - "@next/swc-darwin-x64": "16.2.3", - "@next/swc-linux-arm64-gnu": "16.2.3", - "@next/swc-linux-arm64-musl": "16.2.3", - "@next/swc-linux-x64-gnu": "16.2.3", - "@next/swc-linux-x64-musl": "16.2.3", - "@next/swc-win32-arm64-msvc": "16.2.3", - "@next/swc-win32-x64-msvc": "16.2.3", - "sharp": "^0.34.5" + "@next/swc-darwin-arm64": "16.4.3", + "@next/swc-darwin-x64": "16.4.3", + "@next/swc-linux-arm64-gnu": "16.4.3", + "@next/swc-linux-arm64-musl": "16.4.3", + "@next/swc-linux-x64-gnu": "16.4.3", + "@next/swc-linux-x64-musl": "16.4.3", + "@next/swc-win32-arm64-msvc": "16.4.3", + "@next/swc-win32-x64-msvc": "16.4.3", + "sharp": "^0.35.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -7096,18 +7156,21 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", - "license": "MIT" + "version": "2.0.61", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.61.tgz", + "integrity": "sha512-5NUN6QCxNFrS9oEA9RPS+d0AP+ooGBzzovY2JhIpDRHacPvOhHjXjUsUMX9qp4nLHZ+5jVgXB2jj/P4arGOE/g==", + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nodemailer": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.6.tgz", - "integrity": "sha512-Nm2XeuDwwy2wi5A+8jPWwQwNzcjNjhWdE3pVLoXEusxJqCnAPAgnBGkSmiLknbnWuOF9qraRpYZjfxqtKZ4tPw==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.3.1.tgz", + "integrity": "sha512-uF0AFsJC07Y1vdW7p580DKAnh7rAO+4UPSSB3mDCPNDufoiaOebV6JmrzpxB14OjEEEiBsNdYoM2Y+fN8M7BBA==", "license": "MIT-0", "engines": { - "node": ">=6.0.0" + "node": ">=20.0.0" } }, "node_modules/nypm": { @@ -7545,9 +7608,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "funding": [ { "type": "opencollective", @@ -7564,18 +7627,18 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", + "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -7683,24 +7746,23 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", - "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", - "long": "^5.0.0" + "long": "^5.3.2" }, "engines": { "node": ">=12.0.0" @@ -7754,9 +7816,9 @@ "license": "MIT" }, "node_modules/quill": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz", - "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz", + "integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==", "license": "BSD-3-Clause", "dependencies": { "eventemitter3": "^5.0.1", @@ -7836,21 +7898,6 @@ "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", "license": "MIT" }, - "node_modules/react-quill-new": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/react-quill-new/-/react-quill-new-3.8.3.tgz", - "integrity": "sha512-c96PYqFTo0pI4R3e79B3rH9LUIce1kIQbmTBu/imJQZk8305ogyLyBqKKjG2UoInDlquXqePSzmBo2aVia3ttw==", - "license": "MIT", - "dependencies": { - "lodash-es": "^4.17.21", - "quill": "~2.0.3" - }, - "peerDependencies": { - "quill-delta": "^5.1.0", - "react": "^16 || ^17 || ^18 || ^19", - "react-dom": "^16 || ^17 || ^18 || ^19" - } - }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", @@ -8182,17 +8229,109 @@ } }, "node_modules/sanitize-html": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.17.3.tgz", - "integrity": "sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==", + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.18.1.tgz", + "integrity": "sha512-2X5lC8fYla8YaT8g73Nj+HcLWXan5dLyCWLgVvaOmfw7JI78IRsyNtIDytTHdn7hBQbIkCah7Zbmd5jabzeDig==", "license": "MIT", "dependencies": { - "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", - "htmlparser2": "^10.1.0", - "is-plain-object": "^5.0.0", + "htmlparser2": "^12.0.0", + "is-plain-object": "^5.1.0", + "launder": "^1.7.2", "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" + "postcss": "^8.5.27" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/sanitize-html/node_modules/dom-serializer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/sanitize-html/node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", + "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "domutils": "^4.0.2", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" } }, "node_modules/scheduler": { @@ -8202,9 +8341,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "devOptional": true, "license": "ISC", "bin": { @@ -8264,48 +8403,53 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shebang-command": { @@ -8332,9 +8476,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "dev": true, "license": "MIT", "engines": { @@ -8421,9 +8565,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9029,9 +9173,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "funding": [ { "type": "opencollective", @@ -9248,9 +9392,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9337,9 +9481,9 @@ } }, "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "dev": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index 34185b1..8aa1e9e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "engines": { - "node": "24.x" + "node": ">=22.23.4" }, "workspaces": [ "admin" @@ -32,22 +32,21 @@ "@prisma/config": "6.19.3", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.19", - "@types/bcryptjs": "^2.4.6", "@types/node": "^22.14.0", - "@types/pg": "^8.16.0", + "@types/pg": "^8.20.0", "@types/sanitize-html": "^2.16.1", "autoprefixer": "^10.4.27", "bcryptjs": "^3.0.3", - "dotenv": "^17.3.1", + "dotenv": "^17.4.1", "jiti": "^2.6.1", "lucide-react": "^0.554.0", - "next": "16.2.3", - "nodemailer": "^8.0.6", - "pg": "^8.19.0", - "postcss": "^8.5.10", + "next": "^16.4.3", + "nodemailer": "^10.3.1", + "pg": "^8.20.0", + "postcss": "^8.5.23", "prisma": "6.19.3", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", "react-hot-toast": "^2.6.0", "react-is": "^19.2.6", "recharts": "^3.8.1", @@ -64,7 +63,13 @@ "seed": "tsx prisma/seed.ts" }, "overrides": { - "postcss": "^8.5.10", + "postcss": "^8.5.23", + "postcss-selector-parser": "^6.1.5", + "deepmerge-ts": "^8.0.0", + "shell-quote": "^1.8.5", + "d3-color": "^3.1.0", + "esbuild": "^0.28.1", + "quill": "2.0.2", "@hono/node-server": "^1.19.13" } }
    Nom complet :${name}${safeName}
    Adresse email :${email}${safeEmail}
    Date d'inscription :