Initial commit: CMS completo com gerenciamento de leads e personalização de tema

This commit is contained in:
Erik
2025-11-26 14:09:21 -03:00
commit aaa1709e41
106 changed files with 26268 additions and 0 deletions

View 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 }
);
}
}

View 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 });
}
}

View 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;
}

View 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 }
);
}
}