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
All checks were successful
Build and Push App / build (push) Successful in 5m59s
This commit is contained in:
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ArrowLeft, Share2, Play, Calendar, User, Building2, MapPin, ArrowRight } from 'lucide-react';
|
||||
import { sanitizeHTML } from '../../../lib/sanitize';
|
||||
import { prisma } from '../../../lib/prisma';
|
||||
import { InterviewType } from '@prisma/client';
|
||||
|
||||
@@ -158,10 +159,10 @@ export default async function AfroLifeDetailPage({ params }: Props) {
|
||||
{/* Description / Article Content / Event Content */}
|
||||
<div className="prose prose-lg prose-orange max-w-none mx-auto text-gray-600 break-words overflow-hidden">
|
||||
{isEvent ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: (event as any).description }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: sanitizeHTML((event as any).description) }} />
|
||||
) : (
|
||||
!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">
|
||||
{(interview as any).excerpt}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Play, FileText, Clock, Mic, Calendar } from 'lucide-react';
|
||||
import { sanitizeHTML } from '../../lib/sanitize';
|
||||
import { prisma } from '../../lib/prisma';
|
||||
import { InterviewType } from '@prisma/client';
|
||||
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'}`}>
|
||||
{item.title}
|
||||
</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>
|
||||
</Link>
|
||||
);
|
||||
|
||||
7
app/api/auth/csrf/route.ts
Normal file
7
app/api/auth/csrf/route.ts
Normal 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 });
|
||||
}
|
||||
@@ -1,9 +1,22 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextResponse, NextRequest } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { sendEmail } from '@/lib/mail';
|
||||
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 {
|
||||
const { email } = await req.json();
|
||||
|
||||
|
||||
@@ -2,7 +2,20 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { email, password } = await request.json();
|
||||
|
||||
|
||||
@@ -3,7 +3,20 @@ import prisma from '@/lib/prisma';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { name, email, password } = await request.json();
|
||||
|
||||
|
||||
@@ -2,7 +2,20 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import crypto from 'crypto';
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { email } = await request.json();
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { NextResponse, NextRequest } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
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 {
|
||||
const { token, password } = await req.json();
|
||||
const { token, password } = await request.json();
|
||||
|
||||
if (!token || !password) {
|
||||
return NextResponse.json({ error: 'Token et mot de passe sont requis' }, { status: 400 });
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ArrowLeft, User, Calendar, Share2 } from 'lucide-react';
|
||||
import { sanitizeHTML } from '../../../lib/sanitize';
|
||||
import { prisma } from '../../../lib/prisma';
|
||||
|
||||
import { Metadata } from 'next';
|
||||
@@ -107,7 +108,7 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
</p>
|
||||
<div
|
||||
className="mt-6"
|
||||
dangerouslySetInnerHTML={{ __html: post.content }}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizeHTML(post.content) }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { getLegalDocument } from '@/lib/legal';
|
||||
import { sanitizeHTML } from '@/lib/sanitize';
|
||||
|
||||
import { getSiteSettings } from '@/lib/settings';
|
||||
import { Metadata } from 'next';
|
||||
@@ -32,7 +33,7 @@ export default async function CGUPage() {
|
||||
|
||||
<div
|
||||
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">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { getLegalDocument } from '@/lib/legal';
|
||||
import { sanitizeHTML } from '@/lib/sanitize';
|
||||
|
||||
import { getSiteSettings } from '@/lib/settings';
|
||||
import { Metadata } from 'next';
|
||||
@@ -32,7 +33,7 @@ export default async function CGVPage() {
|
||||
|
||||
<div
|
||||
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">
|
||||
|
||||
@@ -7,10 +7,19 @@ import { useUser } from '@/components/UserProvider';
|
||||
const ForgotPasswordPage = () => {
|
||||
const { settings } = useUser();
|
||||
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 [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [csrfToken, setCsrfToken] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -21,7 +30,10 @@ const ForgotPasswordPage = () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/forgot-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-csrf-token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
@@ -61,6 +73,7 @@ const ForgotPasswordPage = () => {
|
||||
</div>
|
||||
) : (
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<input type="hidden" name="csrf_token" value={csrfToken} />
|
||||
{error && (
|
||||
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm">
|
||||
{error}
|
||||
|
||||
@@ -17,11 +17,20 @@ const LoginPage = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState(false);
|
||||
const [resendMessage, setResendMessage] = useState('');
|
||||
const [csrfToken, setCsrfToken] = useState('');
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const verified = searchParams.get('verified') === 'true';
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
@@ -33,7 +42,10 @@ const LoginPage = () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-csrf-token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
@@ -64,7 +76,10 @@ const LoginPage = () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/resend-verification', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-csrf-token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
@@ -131,6 +146,7 @@ const LoginPage = () => {
|
||||
)}
|
||||
|
||||
<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>
|
||||
<input
|
||||
|
||||
@@ -19,11 +19,20 @@ const RegisterPage = () => {
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [csrfToken, setCsrfToken] = useState('');
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
@@ -39,7 +48,10 @@ const RegisterPage = () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-csrf-token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
@@ -87,6 +99,7 @@ const RegisterPage = () => {
|
||||
</div>
|
||||
) : (
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<input type="hidden" name="csrf_token" value={csrfToken} />
|
||||
{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">
|
||||
<AlertCircle size={20} className="shrink-0" />
|
||||
|
||||
@@ -13,9 +13,18 @@ const ResetPasswordForm = () => {
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [csrfToken, setCsrfToken] = useState('');
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
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(() => {
|
||||
if (!token) {
|
||||
@@ -43,7 +52,10 @@ const ResetPasswordForm = () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/reset-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-csrf-token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ token, password }),
|
||||
});
|
||||
|
||||
@@ -84,6 +96,7 @@ const ResetPasswordForm = () => {
|
||||
</div>
|
||||
) : (
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<input type="hidden" name="csrf_token" value={csrfToken} />
|
||||
{error && (
|
||||
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 text-sm">
|
||||
{error}
|
||||
|
||||
Reference in New Issue
Block a user