feat: implement full authentication system with CSRF protection and email verification flow
All checks were successful
Build and Push App / build (push) Successful in 5m59s

This commit is contained in:
2026-04-30 23:46:34 +02:00
parent 4e881bcbe6
commit 9c003d1b7d
21 changed files with 762 additions and 19 deletions

View File

@@ -2,6 +2,7 @@ import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { ArrowLeft, Share2, Play, Calendar, User, Building2, MapPin, ArrowRight } from 'lucide-react'; import { ArrowLeft, Share2, Play, Calendar, User, Building2, MapPin, ArrowRight } from 'lucide-react';
import { sanitizeHTML } from '../../../lib/sanitize';
import { prisma } from '../../../lib/prisma'; import { prisma } from '../../../lib/prisma';
import { InterviewType } from '@prisma/client'; import { InterviewType } from '@prisma/client';
@@ -158,10 +159,10 @@ export default async function AfroLifeDetailPage({ params }: Props) {
{/* Description / Article Content / Event Content */} {/* Description / Article Content / Event Content */}
<div className="prose prose-lg prose-orange max-w-none mx-auto text-gray-600 break-words overflow-hidden"> <div className="prose prose-lg prose-orange max-w-none mx-auto text-gray-600 break-words overflow-hidden">
{isEvent ? ( {isEvent ? (
<div dangerouslySetInnerHTML={{ __html: (event as any).description }} /> <div dangerouslySetInnerHTML={{ __html: sanitizeHTML((event as any).description) }} />
) : ( ) : (
!isVideo && (interview as any).content ? ( !isVideo && (interview as any).content ? (
<div dangerouslySetInnerHTML={{ __html: (interview as any).content }} /> <div dangerouslySetInnerHTML={{ __html: sanitizeHTML((interview as any).content) }} />
) : ( ) : (
<p className="text-xl font-serif italic text-gray-500 border-l-4 border-brand-500 pl-6 py-2"> <p className="text-xl font-serif italic text-gray-500 border-l-4 border-brand-500 pl-6 py-2">
{(interview as any).excerpt} {(interview as any).excerpt}

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { Play, FileText, Clock, Mic, Calendar } from 'lucide-react'; import { Play, FileText, Clock, Mic, Calendar } from 'lucide-react';
import { sanitizeHTML } from '../../lib/sanitize';
import { prisma } from '../../lib/prisma'; import { prisma } from '../../lib/prisma';
import { InterviewType } from '@prisma/client'; import { InterviewType } from '@prisma/client';
import EventTimeline from '../../components/EventTimeline'; import EventTimeline from '../../components/EventTimeline';
@@ -169,7 +170,7 @@ export default async function AfroLifePage({ searchParams }: Props) {
<h3 className={`text-xl font-serif font-bold mb-2 transition-colors leading-tight line-clamp-2 ${isPastEvent ? 'text-gray-500' : 'text-gray-900 group-hover:text-brand-600'}`}> <h3 className={`text-xl font-serif font-bold mb-2 transition-colors leading-tight line-clamp-2 ${isPastEvent ? 'text-gray-500' : 'text-gray-900 group-hover:text-brand-600'}`}>
{item.title} {item.title}
</h3> </h3>
<div className="text-gray-600 text-sm line-clamp-2 prose-sm" dangerouslySetInnerHTML={{ __html: item.excerpt }} /> <div className="text-gray-600 text-sm line-clamp-2 prose-sm" dangerouslySetInnerHTML={{ __html: sanitizeHTML(item.excerpt) }} />
</div> </div>
</Link> </Link>
); );

View File

@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';
import { generateCsrfToken } from '@/lib/csrf';
export async function GET() {
const token = await generateCsrfToken();
return NextResponse.json({ csrfToken: token });
}

View File

@@ -1,9 +1,22 @@
import { NextResponse } from 'next/server'; import { NextResponse, NextRequest } from 'next/server';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import { sendEmail } from '@/lib/mail'; import { sendEmail } from '@/lib/mail';
import crypto from 'crypto'; import crypto from 'crypto';
export async function POST(req: Request) { import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf';
export async function POST(req: NextRequest) {
// CSRF Protection
const isOriginValid = verifyOrigin(req);
const isCsrfValid = await verifyCsrfToken(req);
if (!isOriginValid || !isCsrfValid) {
return NextResponse.json(
{ error: 'Protection CSRF : Requête invalide' },
{ status: 403 }
);
}
try { try {
const { email } = await req.json(); const { email } = await req.json();

View File

@@ -2,7 +2,20 @@ import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
// CSRF Protection
const isOriginValid = verifyOrigin(request);
const isCsrfValid = await verifyCsrfToken(request);
if (!isOriginValid || !isCsrfValid) {
return NextResponse.json(
{ error: 'Protection CSRF : Requête invalide' },
{ status: 403 }
);
}
try { try {
const { email, password } = await request.json(); const { email, password } = await request.json();

View File

@@ -3,7 +3,20 @@ import prisma from '@/lib/prisma';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import crypto from 'crypto'; import crypto from 'crypto';
import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
// CSRF Protection
const isOriginValid = verifyOrigin(request);
const isCsrfValid = await verifyCsrfToken(request);
if (!isOriginValid || !isCsrfValid) {
return NextResponse.json(
{ error: 'Protection CSRF : Requête invalide' },
{ status: 403 }
);
}
try { try {
const { name, email, password } = await request.json(); const { name, email, password } = await request.json();

View File

@@ -2,7 +2,20 @@ import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import crypto from 'crypto'; import crypto from 'crypto';
import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
// CSRF Protection
const isOriginValid = verifyOrigin(request);
const isCsrfValid = await verifyCsrfToken(request);
if (!isOriginValid || !isCsrfValid) {
return NextResponse.json(
{ error: 'Protection CSRF : Requête invalide' },
{ status: 403 }
);
}
try { try {
const { email } = await request.json(); const { email } = await request.json();

View File

@@ -1,10 +1,22 @@
import { NextResponse } from 'next/server'; import { NextResponse, NextRequest } from 'next/server';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { verifyCsrfToken, verifyOrigin } from '@/lib/csrf';
export async function POST(request: NextRequest) {
// CSRF Protection
const isOriginValid = verifyOrigin(request);
const isCsrfValid = await verifyCsrfToken(request);
if (!isOriginValid || !isCsrfValid) {
return NextResponse.json(
{ error: 'Protection CSRF : Requête invalide' },
{ status: 403 }
);
}
export async function POST(req: Request) {
try { try {
const { token, password } = await req.json(); const { token, password } = await request.json();
if (!token || !password) { if (!token || !password) {
return NextResponse.json({ error: 'Token et mot de passe sont requis' }, { status: 400 }); return NextResponse.json({ error: 'Token et mot de passe sont requis' }, { status: 400 });

View File

@@ -2,6 +2,7 @@ import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { ArrowLeft, User, Calendar, Share2 } from 'lucide-react'; import { ArrowLeft, User, Calendar, Share2 } from 'lucide-react';
import { sanitizeHTML } from '../../../lib/sanitize';
import { prisma } from '../../../lib/prisma'; import { prisma } from '../../../lib/prisma';
import { Metadata } from 'next'; import { Metadata } from 'next';
@@ -107,7 +108,7 @@ export default async function BlogPostPage({ params }: Props) {
</p> </p>
<div <div
className="mt-6" className="mt-6"
dangerouslySetInnerHTML={{ __html: post.content }} dangerouslySetInnerHTML={{ __html: sanitizeHTML(post.content) }}
/> />
</div> </div>

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { getLegalDocument } from '@/lib/legal'; import { getLegalDocument } from '@/lib/legal';
import { sanitizeHTML } from '@/lib/sanitize';
import { getSiteSettings } from '@/lib/settings'; import { getSiteSettings } from '@/lib/settings';
import { Metadata } from 'next'; import { Metadata } from 'next';
@@ -32,7 +33,7 @@ export default async function CGUPage() {
<div <div
className="prose prose-orange max-w-none text-gray-600 space-y-6" className="prose prose-orange max-w-none text-gray-600 space-y-6"
dangerouslySetInnerHTML={{ __html: doc.content }} dangerouslySetInnerHTML={{ __html: sanitizeHTML(doc.content) }}
/> />
<div className="mt-12 pt-8 border-t border-gray-100 text-xs text-gray-400"> <div className="mt-12 pt-8 border-t border-gray-100 text-xs text-gray-400">

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { getLegalDocument } from '@/lib/legal'; import { getLegalDocument } from '@/lib/legal';
import { sanitizeHTML } from '@/lib/sanitize';
import { getSiteSettings } from '@/lib/settings'; import { getSiteSettings } from '@/lib/settings';
import { Metadata } from 'next'; import { Metadata } from 'next';
@@ -32,7 +33,7 @@ export default async function CGVPage() {
<div <div
className="prose prose-orange max-w-none text-gray-600 space-y-6" className="prose prose-orange max-w-none text-gray-600 space-y-6"
dangerouslySetInnerHTML={{ __html: doc.content }} dangerouslySetInnerHTML={{ __html: sanitizeHTML(doc.content) }}
/> />
<div className="mt-12 pt-8 border-t border-gray-100 text-xs text-gray-400"> <div className="mt-12 pt-8 border-t border-gray-100 text-xs text-gray-400">

View File

@@ -7,10 +7,19 @@ import { useUser } from '@/components/UserProvider';
const ForgotPasswordPage = () => { const ForgotPasswordPage = () => {
const { settings } = useUser(); const { settings } = useUser();
const siteName = settings?.siteName || "Afrohub"; const siteName = settings?.siteName || "Afrohub";
// Fetch CSRF token on mount
React.useEffect(() => {
fetch('/api/auth/csrf')
.then(res => res.json())
.then(data => setCsrfToken(data.csrfToken))
.catch(err => console.error('Error fetching CSRF token:', err));
}, []);
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [csrfToken, setCsrfToken] = useState('');
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -21,7 +30,10 @@ const ForgotPasswordPage = () => {
try { try {
const res = await fetch('/api/auth/forgot-password', { const res = await fetch('/api/auth/forgot-password', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'x-csrf-token': csrfToken
},
body: JSON.stringify({ email }), body: JSON.stringify({ email }),
}); });
@@ -61,6 +73,7 @@ const ForgotPasswordPage = () => {
</div> </div>
) : ( ) : (
<form className="mt-8 space-y-6" onSubmit={handleSubmit}> <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<input type="hidden" name="csrf_token" value={csrfToken} />
{error && ( {error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm"> <div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm">
{error} {error}

View File

@@ -17,12 +17,21 @@ const LoginPage = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [resendLoading, setResendLoading] = useState(false); const [resendLoading, setResendLoading] = useState(false);
const [resendMessage, setResendMessage] = useState(''); const [resendMessage, setResendMessage] = useState('');
const [csrfToken, setCsrfToken] = useState('');
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const verified = searchParams.get('verified') === 'true'; const verified = searchParams.get('verified') === 'true';
const queryError = searchParams.get('error'); const queryError = searchParams.get('error');
// Fetch CSRF token on mount
React.useEffect(() => {
fetch('/api/auth/csrf')
.then(res => res.json())
.then(data => setCsrfToken(data.csrfToken))
.catch(err => console.error('Error fetching CSRF token:', err));
}, []);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
@@ -33,7 +42,10 @@ const LoginPage = () => {
try { try {
const res = await fetch('/api/auth/login', { const res = await fetch('/api/auth/login', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'x-csrf-token': csrfToken
},
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
}); });
@@ -64,7 +76,10 @@ const LoginPage = () => {
try { try {
const res = await fetch('/api/auth/resend-verification', { const res = await fetch('/api/auth/resend-verification', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'x-csrf-token': csrfToken
},
body: JSON.stringify({ email }), body: JSON.stringify({ email }),
}); });
@@ -131,6 +146,7 @@ const LoginPage = () => {
)} )}
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleSubmit}>
<input type="hidden" name="csrf_token" value={csrfToken} />
<div className="rounded-md shadow-sm -space-y-px"> <div className="rounded-md shadow-sm -space-y-px">
<div> <div>
<input <input

View File

@@ -19,12 +19,21 @@ const RegisterPage = () => {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
const [csrfToken, setCsrfToken] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value }); setFormData({ ...formData, [e.target.name]: e.target.value });
setError(''); setError('');
}; };
// Fetch CSRF token on mount
React.useEffect(() => {
fetch('/api/auth/csrf')
.then(res => res.json())
.then(data => setCsrfToken(data.csrfToken))
.catch(err => console.error('Error fetching CSRF token:', err));
}, []);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
@@ -39,7 +48,10 @@ const RegisterPage = () => {
try { try {
const res = await fetch('/api/auth/register', { const res = await fetch('/api/auth/register', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'x-csrf-token': csrfToken
},
body: JSON.stringify({ body: JSON.stringify({
name: formData.name, name: formData.name,
email: formData.email, email: formData.email,
@@ -87,6 +99,7 @@ const RegisterPage = () => {
</div> </div>
) : ( ) : (
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleSubmit}>
<input type="hidden" name="csrf_token" value={csrfToken} />
{error && ( {error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded-r-lg flex items-center gap-3 text-red-700 animate-in slide-in-from-top-2 duration-300"> <div className="bg-red-50 border-l-4 border-red-500 p-4 rounded-r-lg flex items-center gap-3 text-red-700 animate-in slide-in-from-top-2 duration-300">
<AlertCircle size={20} className="shrink-0" /> <AlertCircle size={20} className="shrink-0" />

View File

@@ -13,10 +13,19 @@ const ResetPasswordForm = () => {
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [csrfToken, setCsrfToken] = useState('');
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const token = searchParams.get('token'); const token = searchParams.get('token');
// Fetch CSRF token on mount
React.useEffect(() => {
fetch('/api/auth/csrf')
.then(res => res.json())
.then(data => setCsrfToken(data.csrfToken))
.catch(err => console.error('Error fetching CSRF token:', err));
}, []);
useEffect(() => { useEffect(() => {
if (!token) { if (!token) {
setError("Le lien de réinitialisation est invalide ou manquant."); setError("Le lien de réinitialisation est invalide ou manquant.");
@@ -43,7 +52,10 @@ const ResetPasswordForm = () => {
try { try {
const res = await fetch('/api/auth/reset-password', { const res = await fetch('/api/auth/reset-password', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
'x-csrf-token': csrfToken
},
body: JSON.stringify({ token, password }), body: JSON.stringify({ token, password }),
}); });
@@ -84,6 +96,7 @@ const ResetPasswordForm = () => {
</div> </div>
) : ( ) : (
<form className="mt-8 space-y-6" onSubmit={handleSubmit}> <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<input type="hidden" name="csrf_token" value={csrfToken} />
{error && ( {error && (
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm"> <div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm">
{error} {error}

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Calendar, MapPin, ArrowRight, Clock } from 'lucide-react'; import { Calendar, MapPin, ArrowRight, Clock } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { sanitizeHTML } from '../lib/sanitize';
interface Event { interface Event {
id: string; id: string;
@@ -74,7 +75,7 @@ const EventTimeline = ({ events }: EventTimelineProps) => {
</div> </div>
<div <div
className="text-gray-600 text-sm line-clamp-2 prose-sm" className="text-gray-600 text-sm line-clamp-2 prose-sm"
dangerouslySetInnerHTML={{ __html: event.description }} dangerouslySetInnerHTML={{ __html: sanitizeHTML(event.description) }}
/> />
<div className="pt-4 flex items-center justify-between"> <div className="pt-4 flex items-center justify-between">

47
lib/csrf.ts Normal file
View File

@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import crypto from 'crypto';
const CSRF_COOKIE_NAME = 'csrf_token';
const CSRF_HEADER_NAME = 'x-csrf-token';
export async function generateCsrfToken() {
const token = crypto.randomBytes(32).toString('hex');
const cookieStore = await cookies();
cookieStore.set(CSRF_COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
});
return token;
}
export async function verifyCsrfToken(request: NextRequest) {
const cookieStore = await cookies();
const cookieToken = cookieStore.get(CSRF_COOKIE_NAME)?.value;
const headerToken = request.headers.get(CSRF_HEADER_NAME);
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return false;
}
return true;
}
/**
* Basic Origin check as an additional layer of defense
*/
export function verifyOrigin(request: NextRequest) {
const origin = request.headers.get('origin');
const host = request.headers.get('host');
if (!origin || !host) return false;
// Remove protocol from origin for comparison if host doesn't have it
const originHost = origin.replace(/^https?:\/\//, '');
return originHost === host || originHost.startsWith(host + ':');
}

17
lib/sanitize.ts Normal file
View File

@@ -0,0 +1,17 @@
import DOMPurify from 'isomorphic-dompurify';
/**
* Sanitizes HTML content to prevent XSS attacks.
* Use this before passing content to dangerouslySetInnerHTML.
*/
export function sanitizeHTML(html: string): string {
return DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
ALLOWED_TAGS: [
'p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'span', 'div',
'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td'
],
ALLOWED_ATTR: ['href', 'target', 'rel', 'src', 'alt', 'class', 'style']
});
}

View File

@@ -23,7 +23,7 @@ const nextConfig = {
}, },
{ {
key: 'Content-Security-Policy', key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' https:; connect-src 'self' wss: https:; base-uri 'self'; form-action 'self'; frame-ancestors 'self';", value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; media-src 'self' https:; connect-src 'self' wss: https:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; object-src 'none';",
}, },
{ {
key: 'X-Content-Type-Options', key: 'X-Content-Type-Options',
@@ -32,6 +32,18 @@ const nextConfig = {
{ {
key: 'Referrer-Policy', key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin', value: 'strict-origin-when-cross-origin',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
} }
], ],
}, },

533
package-lock.json generated
View File

@@ -15,11 +15,13 @@
"@tailwindcss/postcss": "^4.2.2", "@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/dompurify": "^3.0.5",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"autoprefixer": "^10.4.27", "autoprefixer": "^10.4.27",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"isomorphic-dompurify": "^3.11.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"lucide-react": "^0.554.0", "lucide-react": "^0.554.0",
"next": "^16.1.6", "next": "^16.1.6",
@@ -55,6 +57,199 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@asamuzakjp/css-color": {
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
"integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/generational-cache": "^1.0.1",
"@csstools/css-calc": "^3.2.0",
"@csstools/css-color-parser": "^4.1.0",
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@asamuzakjp/dom-selector": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/generational-cache": "^1.0.1",
"@asamuzakjp/nwsapi": "^2.3.9",
"bidi-js": "^1.0.3",
"css-tree": "^3.2.1",
"is-potential-custom-element-name": "^1.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@asamuzakjp/generational-cache": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
"integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
"license": "MIT",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@asamuzakjp/nwsapi": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
"license": "MIT"
},
"node_modules/@bramus/specificity": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
"license": "MIT",
"dependencies": {
"css-tree": "^3.0.0"
},
"bin": {
"specificity": "bin/cli.js"
}
},
"node_modules/@csstools/color-helpers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
"integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/@csstools/css-calc": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz",
"integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz",
"integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^6.0.2",
"@csstools/css-calc": "^3.2.0"
},
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz",
"integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"peerDependencies": {
"css-tree": "^3.2.1"
},
"peerDependenciesMeta": {
"css-tree": {
"optional": true
}
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
@@ -481,6 +676,23 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@exodus/bytes": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
"integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
"license": "MIT",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@noble/hashes": "^1.8.0 || ^2.0.0"
},
"peerDependenciesMeta": {
"@noble/hashes": {
"optional": true
}
}
},
"node_modules/@google/genai": { "node_modules/@google/genai": {
"version": "1.50.1", "version": "1.50.1",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.50.1.tgz", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.50.1.tgz",
@@ -1706,6 +1918,15 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"license": "MIT",
"dependencies": {
"@types/trusted-types": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.19.17", "version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
@@ -1753,6 +1974,12 @@
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@types/use-sync-external-store": { "node_modules/@types/use-sync-external-store": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -1871,6 +2098,15 @@
"bcrypt": "bin/bcrypt" "bcrypt": "bin/bcrypt"
} }
}, },
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"license": "MIT",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/bignumber.js": { "node_modules/bignumber.js": {
"version": "9.3.1", "version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
@@ -2123,6 +2359,19 @@
"node": "^14.18.0 || >=16.10.0" "node": "^14.18.0 || >=16.10.0"
} }
}, },
"node_modules/css-tree": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.27.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2271,6 +2520,19 @@
"node": ">= 12" "node": ">= 12"
} }
}, },
"node_modules/data-urls": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^5.0.0",
"whatwg-url": "^16.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2288,6 +2550,12 @@
} }
} }
}, },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/decimal.js-light": { "node_modules/decimal.js-light": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@@ -2324,6 +2592,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dompurify": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz",
"integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "17.4.2", "version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
@@ -2390,6 +2667,18 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/entities": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-toolkit": { "node_modules/es-toolkit": {
"version": "1.46.0", "version": "1.46.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.0.tgz", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.0.tgz",
@@ -2670,6 +2959,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/html-encoding-sniffer": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
"license": "MIT",
"dependencies": {
"@exodus/bytes": "^1.6.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/https-proxy-agent": { "node_modules/https-proxy-agent": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -2712,6 +3013,25 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"license": "MIT"
},
"node_modules/isomorphic-dompurify": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-3.11.0.tgz",
"integrity": "sha512-il0sNhLnfawc6vKoMqnZX1JXzEzw0pP3DZWg7mM7VJNJ8cq6DCxeAjEcfjTazyBF8dkUn+rBsBPS5NQs4ZaI3g==",
"license": "MIT",
"dependencies": {
"dompurify": "^3.4.1",
"jsdom": "^29.1.0"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
}
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.6.1", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -2721,6 +3041,46 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/jsdom": {
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^5.1.11",
"@asamuzakjp/dom-selector": "^7.1.1",
"@bramus/specificity": "^2.4.2",
"@csstools/css-syntax-patches-for-csstree": "^1.1.3",
"@exodus/bytes": "^1.15.0",
"css-tree": "^3.2.1",
"data-urls": "^7.0.0",
"decimal.js": "^10.6.0",
"html-encoding-sniffer": "^6.0.0",
"is-potential-custom-element-name": "^1.0.1",
"lru-cache": "^11.3.5",
"parse5": "^8.0.1",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^6.0.1",
"undici": "^7.25.0",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^8.0.1",
"whatwg-mimetype": "^5.0.0",
"whatwg-url": "^16.0.1",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/json-bigint": { "node_modules/json-bigint": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
@@ -3006,6 +3366,15 @@
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/lru-cache": {
"version": "11.3.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
"integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/lucide-react": { "node_modules/lucide-react": {
"version": "0.554.0", "version": "0.554.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz",
@@ -3024,6 +3393,12 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/mdn-data": {
"version": "2.27.1",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
"license": "CC0-1.0"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3202,6 +3577,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/parse5": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
"license": "MIT",
"dependencies": {
"entities": "^8.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/pathe": { "node_modules/pathe": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -3464,6 +3851,15 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/pure-rand": { "node_modules/pure-rand": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
@@ -3626,6 +4022,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/reselect": { "node_modules/reselect": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@@ -3680,6 +4085,18 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -3842,6 +4259,12 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"license": "MIT"
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
@@ -3876,6 +4299,48 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/tldts": {
"version": "7.0.29",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.29.tgz",
"integrity": "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==",
"license": "MIT",
"dependencies": {
"tldts-core": "^7.0.29"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "7.0.29",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.29.tgz",
"integrity": "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==",
"license": "MIT"
},
"node_modules/tough-cookie": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^7.0.5"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/tree-kill": { "node_modules/tree-kill": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -3924,6 +4389,15 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici": {
"version": "7.25.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -3997,6 +4471,18 @@
"d3-timer": "^3.0.1" "d3-timer": "^3.0.1"
} }
}, },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/web-streams-polyfill": { "node_modules/web-streams-polyfill": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
@@ -4006,6 +4492,38 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/webidl-conversions": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=20"
}
},
"node_modules/whatwg-mimetype": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/whatwg-url": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
"license": "MIT",
"dependencies": {
"@exodus/bytes": "^1.11.0",
"tr46": "^6.0.0",
"webidl-conversions": "^8.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -4045,6 +4563,21 @@
} }
} }
}, },
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -28,11 +28,13 @@
"@tailwindcss/postcss": "^4.2.2", "@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/dompurify": "^3.0.5",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/pg": "^8.16.0", "@types/pg": "^8.16.0",
"autoprefixer": "^10.4.27", "autoprefixer": "^10.4.27",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"isomorphic-dompurify": "^3.11.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"lucide-react": "^0.554.0", "lucide-react": "^0.554.0",
"next": "^16.1.6", "next": "^16.1.6",