Initial commit: CMS completo com gerenciamento de leads e personalização de tema
This commit is contained in:
179
frontend/src/app/api/auth/avatar/route.ts
Normal file
179
frontend/src/app/api/auth/avatar/route.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { minioClient, ensureBucketExists } from '@/lib/minio';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||||
const BUCKET_NAME = process.env.MINIO_BUCKET_NAME || 'occto-images';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Ensure bucket exists
|
||||
await ensureBucketExists();
|
||||
|
||||
const token = request.cookies.get('auth_token')?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Não autenticado' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify JWT token
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
||||
|
||||
// Get form data
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('avatar') as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Nenhum arquivo enviado' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/jpg'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Tipo de arquivo inválido. Use JPEG, PNG ou WEBP' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
const maxSize = 5 * 1024 * 1024; // 5MB
|
||||
if (file.size > maxSize) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Arquivo muito grande. Tamanho máximo: 5MB' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
const fileName = `avatars/${decoded.userId}/${uuidv4()}.${fileExtension}`;
|
||||
|
||||
// Convert file to buffer
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// Upload to MinIO
|
||||
await minioClient.putObject(
|
||||
BUCKET_NAME,
|
||||
fileName,
|
||||
buffer,
|
||||
buffer.length,
|
||||
{
|
||||
'Content-Type': file.type,
|
||||
}
|
||||
);
|
||||
|
||||
// Generate public URL
|
||||
const protocol = process.env.MINIO_USE_SSL === 'true' ? 'https' : 'http';
|
||||
const endpoint = process.env.MINIO_ENDPOINT || 'localhost';
|
||||
const port = process.env.MINIO_PORT || '9000';
|
||||
const avatarUrl = `${protocol}://${endpoint}:${port}/${BUCKET_NAME}/${fileName}`;
|
||||
|
||||
// Delete old avatar if exists
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
select: { avatar: true },
|
||||
});
|
||||
|
||||
if (user?.avatar) {
|
||||
try {
|
||||
// Extract filename from URL
|
||||
const oldFileName = user.avatar.split(`${BUCKET_NAME}/`)[1];
|
||||
if (oldFileName) {
|
||||
await minioClient.removeObject(BUCKET_NAME, oldFileName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting old avatar:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update user avatar in database
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: decoded.userId },
|
||||
data: { avatar: avatarUrl },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Avatar atualizado com sucesso',
|
||||
user: updatedUser,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error uploading avatar:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao fazer upload do avatar' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const token = request.cookies.get('auth_token')?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Não autenticado' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify JWT token
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
||||
|
||||
// Get user's current avatar
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
select: { avatar: true },
|
||||
});
|
||||
|
||||
if (user?.avatar) {
|
||||
try {
|
||||
// Extract filename from URL
|
||||
const fileName = user.avatar.split(`${BUCKET_NAME}/`)[1];
|
||||
if (fileName) {
|
||||
await minioClient.removeObject(BUCKET_NAME, fileName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting avatar from MinIO:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove avatar from database
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: decoded.userId },
|
||||
data: { avatar: null },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Avatar removido com sucesso',
|
||||
user: updatedUser,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting avatar:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao remover avatar' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
95
frontend/src/app/api/auth/login/route.ts
Normal file
95
frontend/src/app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import prisma from '@/lib/prisma';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-CHANGE-IN-PRODUCTION';
|
||||
const MAX_LOGIN_ATTEMPTS = 5;
|
||||
const LOCKOUT_TIME = 15 * 60 * 1000; // 15 minutos
|
||||
|
||||
// Rate limiting simples (em produção, use Redis)
|
||||
const loginAttempts = new Map<string, { count: number; lastAttempt: number }>();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { email, password } = await request.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json({ error: 'Email e senha são obrigatórios' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Rate limiting básico
|
||||
const attempts = loginAttempts.get(email);
|
||||
if (attempts) {
|
||||
if (attempts.count >= MAX_LOGIN_ATTEMPTS) {
|
||||
const timeSinceLastAttempt = Date.now() - attempts.lastAttempt;
|
||||
if (timeSinceLastAttempt < LOCKOUT_TIME) {
|
||||
const minutesLeft = Math.ceil((LOCKOUT_TIME - timeSinceLastAttempt) / 60000);
|
||||
return NextResponse.json(
|
||||
{ error: `Muitas tentativas. Tente novamente em ${minutesLeft} minutos.` },
|
||||
{ status: 429 }
|
||||
);
|
||||
} else {
|
||||
loginAttempts.delete(email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar usuário no banco
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
// Proteção contra timing attacks - sempre fazer hash mesmo se usuário não existir
|
||||
const userPassword = user?.password || '$2a$10$dummyHashToPreventTimingAttack';
|
||||
const passwordMatch = await bcrypt.compare(password, userPassword);
|
||||
|
||||
if (!user || !passwordMatch) {
|
||||
// Incrementar tentativas
|
||||
const current = loginAttempts.get(email) || { count: 0, lastAttempt: 0 };
|
||||
loginAttempts.set(email, {
|
||||
count: current.count + 1,
|
||||
lastAttempt: Date.now(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ error: 'Credenciais inválidas' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Limpar tentativas de login após sucesso
|
||||
loginAttempts.delete(email);
|
||||
|
||||
// Criar JWT
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
// Criar resposta com cookie de sessão
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name
|
||||
}
|
||||
});
|
||||
|
||||
// Definir cookie de autenticação com JWT
|
||||
response.cookies.set('auth_token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 dias
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Erro no login:', error);
|
||||
return NextResponse.json({ error: 'Erro ao fazer login' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
10
frontend/src/app/api/auth/logout/route.ts
Normal file
10
frontend/src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST() {
|
||||
const response = NextResponse.json({ success: true });
|
||||
|
||||
// Remover cookie de autenticação
|
||||
response.cookies.delete('auth_token');
|
||||
|
||||
return response;
|
||||
}
|
||||
48
frontend/src/app/api/auth/me/route.ts
Normal file
48
frontend/src/app/api/auth/me/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const token = request.cookies.get('auth_token')?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Não autenticado' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify JWT token
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
||||
|
||||
// Get user from database
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Usuário não encontrado' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ user });
|
||||
} catch (error) {
|
||||
console.error('Error fetching user data:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erro ao buscar dados do usuário' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user