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

View File

@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET() {
try {
const config = await prisma.pageContent.findUnique({
where: { slug: 'config' }
});
if (!config) {
return NextResponse.json({ primaryColor: '#FF6B35' });
}
return NextResponse.json(config.content);
} catch (error) {
console.error('Error fetching config:', error);
return NextResponse.json({ primaryColor: '#FF6B35' });
}
}
export async function PUT(request: NextRequest) {
try {
const { primaryColor } = await request.json();
const config = await prisma.pageContent.upsert({
where: { slug: 'config' },
update: {
content: { primaryColor }
},
create: {
slug: 'config',
content: { primaryColor }
}
});
return NextResponse.json({ success: true, config });
} catch (error) {
console.error('Error updating config:', error);
return NextResponse.json({ error: 'Failed to update config' }, { status: 500 });
}
}

View File

@@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const message = await prisma.message.findUnique({
where: { id },
});
if (!message) return NextResponse.json({ error: 'Message not found' }, { status: 404 });
return NextResponse.json(message);
} catch (error) {
return NextResponse.json({ error: 'Error fetching message' }, { status: 500 });
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const data = await request.json();
const message = await prisma.message.update({
where: { id },
data: {
status: data.status,
},
});
return NextResponse.json(message);
} catch (error) {
return NextResponse.json({ error: 'Error updating message' }, { status: 500 });
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
await prisma.message.delete({
where: { id },
});
return NextResponse.json({ message: 'Message deleted' });
} catch (error) {
return NextResponse.json({ error: 'Error deleting message' }, { status: 500 });
}
}

View File

@@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const search = searchParams.get('search') || '';
const status = searchParams.get('status') || '';
const where: any = {};
// Filtro de pesquisa
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
{ subject: { contains: search, mode: 'insensitive' } },
{ message: { contains: search, mode: 'insensitive' } }
];
}
// Filtro de status
if (status) {
where.status = status;
}
const messages = await prisma.message.findMany({
where,
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(messages);
} catch (error) {
return NextResponse.json({ error: 'Error fetching messages' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const { name, email, phone, subject, message } = await request.json();
// Validação básica
if (!name || !email || !message) {
return NextResponse.json(
{ error: 'Nome, email e mensagem são obrigatórios' },
{ status: 400 }
);
}
// Criar mensagem incluindo telefone no assunto se fornecido
const fullSubject = phone
? `${subject || 'Sem assunto'} | Tel: ${phone}`
: subject || 'Sem assunto';
const newMessage = await prisma.message.create({
data: {
name,
email,
subject: fullSubject,
message,
status: 'Nova'
}
});
return NextResponse.json(newMessage, { status: 201 });
} catch (error) {
console.error('Error creating message:', error);
return NextResponse.json({ error: 'Error creating message' }, { status: 500 });
}
}

View File

@@ -0,0 +1,104 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import { cookies } from 'next/headers';
import jwt from 'jsonwebtoken';
// Middleware de autenticação
async function authenticate() {
const cookieStore = await cookies();
const token = cookieStore.get('auth_token')?.value;
if (!token) {
return null;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, email: true, name: true }
});
return user;
} catch (error) {
return null;
}
}
// GET /api/pages/[slug] - Buscar página específica (público)
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = await params;
const page = await prisma.pageContent.findUnique({
where: { slug }
});
if (!page) {
return NextResponse.json({ error: 'Página não encontrada' }, { status: 404 });
}
return NextResponse.json(page);
} catch (error) {
console.error('Erro ao buscar página:', error);
return NextResponse.json({ error: 'Erro ao buscar página' }, { status: 500 });
}
}
// PUT /api/pages/[slug] - Atualizar página (admin apenas)
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const user = await authenticate();
if (!user) {
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 });
}
const { slug } = await params;
const body = await request.json();
const { content } = body;
if (!content) {
return NextResponse.json({ error: 'Conteúdo é obrigatório' }, { status: 400 });
}
const page = await prisma.pageContent.upsert({
where: { slug },
update: { content },
create: { slug, content }
});
return NextResponse.json({ success: true, page });
} catch (error) {
console.error('Erro ao atualizar página:', error);
return NextResponse.json({ error: 'Erro ao atualizar página' }, { status: 500 });
}
}
// DELETE /api/pages/[slug] - Deletar página (admin apenas)
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const user = await authenticate();
if (!user) {
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 });
}
const { slug } = await params;
await prisma.pageContent.delete({
where: { slug }
});
return NextResponse.json({ success: true, message: 'Página deletada com sucesso' });
} catch (error) {
console.error('Erro ao deletar página:', error);
return NextResponse.json({ error: 'Erro ao deletar página' }, { status: 500 });
}
}

View File

@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET() {
try {
const page = await prisma.pageContent.findUnique({
where: { slug: 'contact' }
});
if (!page) {
return NextResponse.json({ content: null }, { status: 404 });
}
return NextResponse.json({ content: page.content });
} catch (error) {
console.error('Error fetching contact page:', error);
return NextResponse.json({ error: 'Failed to fetch page' }, { status: 500 });
}
}
export async function PUT(request: NextRequest) {
try {
const { content } = await request.json();
const page = await prisma.pageContent.upsert({
where: { slug: 'contact' },
update: {
content
},
create: {
slug: 'contact',
content
}
});
return NextResponse.json({ success: true, page });
} catch (error) {
console.error('Error updating contact page:', error);
return NextResponse.json({ error: 'Failed to update page' }, { status: 500 });
}
}

View File

@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { cookies } from 'next/headers';
import jwt from 'jsonwebtoken';
const prisma = new PrismaClient();
// Middleware de autenticação
async function authenticate(request: NextRequest) {
const cookieStore = await cookies();
const token = cookieStore.get('auth_token')?.value;
if (!token) {
return null;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, email: true, name: true }
});
return user;
} catch (error) {
return null;
}
}
// GET /api/pages - Listar todas as páginas (público)
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const slug = searchParams.get('slug');
if (slug) {
// Buscar página específica
const page = await prisma.pageContent.findUnique({
where: { slug }
});
if (!page) {
return NextResponse.json({ error: 'Página não encontrada' }, { status: 404 });
}
return NextResponse.json(page);
}
// Listar todas as páginas
const pages = await prisma.pageContent.findMany({
orderBy: { slug: 'asc' }
});
return NextResponse.json(pages);
} catch (error) {
console.error('Erro ao buscar páginas:', error);
return NextResponse.json({ error: 'Erro ao buscar páginas' }, { status: 500 });
}
}
// POST /api/pages - Criar ou atualizar página (admin apenas)
export async function POST(request: NextRequest) {
try {
const user = await authenticate(request);
if (!user) {
return NextResponse.json({ error: 'Não autorizado' }, { status: 401 });
}
const body = await request.json();
const { slug, content } = body;
if (!slug || !content) {
return NextResponse.json({ error: 'Slug e conteúdo são obrigatórios' }, { status: 400 });
}
// Upsert: criar ou atualizar se já existir
const page = await prisma.pageContent.upsert({
where: { slug },
update: { content },
create: { slug, content }
});
return NextResponse.json({ success: true, page });
} catch (error) {
console.error('Erro ao salvar página:', error);
return NextResponse.json({ error: 'Erro ao salvar página' }, { status: 500 });
}
}

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const project = await prisma.project.findUnique({
where: { id },
});
if (!project) return NextResponse.json({ error: 'Project not found' }, { status: 404 });
return NextResponse.json(project);
} catch (error) {
return NextResponse.json({ error: 'Error fetching project' }, { status: 500 });
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const data = await request.json();
const project = await prisma.project.update({
where: { id },
data: {
title: data.title,
category: data.category,
client: data.client,
status: data.status,
completionDate: data.completionDate ? new Date(data.completionDate) : null,
description: data.description,
coverImage: data.coverImage,
galleryImages: data.galleryImages,
featured: data.featured,
},
});
return NextResponse.json(project);
} catch (error) {
return NextResponse.json({ error: 'Error updating project' }, { status: 500 });
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
await prisma.project.delete({
where: { id },
});
return NextResponse.json({ message: 'Project deleted' });
} catch (error) {
return NextResponse.json({ error: 'Error deleting project' }, { status: 500 });
}
}

View File

@@ -0,0 +1,37 @@
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET() {
try {
const projects = await prisma.project.findMany({
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(projects);
} catch (error) {
console.error('Error fetching projects:', error);
return NextResponse.json({ error: 'Error fetching projects' }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const data = await request.json();
const project = await prisma.project.create({
data: {
title: data.title,
category: data.category,
client: data.client,
status: data.status,
completionDate: data.completionDate ? new Date(data.completionDate) : null,
description: data.description,
coverImage: data.coverImage,
galleryImages: data.galleryImages,
featured: data.featured,
},
});
return NextResponse.json(project);
} catch (error) {
console.error('Error creating project:', error);
return NextResponse.json({ error: 'Error creating project' }, { status: 500 });
}
}

View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const service = await prisma.service.findUnique({
where: { id },
});
if (!service) return NextResponse.json({ error: 'Service not found' }, { status: 404 });
return NextResponse.json(service);
} catch (error) {
return NextResponse.json({ error: 'Error fetching service' }, { status: 500 });
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const data = await request.json();
const service = await prisma.service.update({
where: { id },
data: {
title: data.title,
icon: data.icon,
shortDescription: data.shortDescription,
fullDescription: data.fullDescription,
active: data.active,
order: data.order,
},
});
return NextResponse.json(service);
} catch (error) {
return NextResponse.json({ error: 'Error updating service' }, { status: 500 });
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
await prisma.service.delete({
where: { id },
});
return NextResponse.json({ message: 'Service deleted' });
} catch (error) {
return NextResponse.json({ error: 'Error deleting service' }, { status: 500 });
}
}

View File

@@ -0,0 +1,32 @@
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET() {
try {
const services = await prisma.service.findMany({
orderBy: { order: 'asc' },
});
return NextResponse.json(services);
} catch (error) {
return NextResponse.json({ error: 'Error fetching services' }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const data = await request.json();
const service = await prisma.service.create({
data: {
title: data.title,
icon: data.icon,
shortDescription: data.shortDescription,
fullDescription: data.fullDescription,
active: data.active,
order: data.order,
},
});
return NextResponse.json(service);
} catch (error) {
return NextResponse.json({ error: 'Error creating service' }, { status: 500 });
}
}

View File

@@ -0,0 +1,32 @@
import { NextResponse } from 'next/server';
import { minioClient, bucketName, ensureBucketExists } from '@/lib/minio';
import { v4 as uuidv4 } from 'uuid';
export async function POST(request: Request) {
try {
await ensureBucketExists();
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json({ error: 'No file uploaded' }, { status: 400 });
}
const buffer = Buffer.from(await file.arrayBuffer());
const filename = `${uuidv4()}-${file.name.replace(/\s+/g, '-')}`; // Sanitize filename
await minioClient.putObject(bucketName, filename, buffer, file.size, {
'Content-Type': file.type,
});
// Construct public URL
// In a real production env, this should be an env var like NEXT_PUBLIC_STORAGE_URL
const url = `http://localhost:9000/${bucketName}/${filename}`;
return NextResponse.json({ url });
} catch (error) {
console.error('Upload error:', error);
return NextResponse.json({ error: 'Error uploading file' }, { status: 500 });
}
}

View File

@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import prisma from '@/lib/prisma';
import { minioClient } from '@/lib/minio';
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 DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const token = request.cookies.get('auth_token')?.value;
if (!token) {
return NextResponse.json(
{ error: 'Não autenticado' },
{ status: 401 }
);
}
// Verify JWT token
jwt.verify(token, JWT_SECRET);
// Get user's current avatar
const user = await prisma.user.findUnique({
where: { id },
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 },
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,96 @@
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import * as bcrypt from 'bcryptjs';
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params;
const user = await prisma.user.findUnique({
where: { id },
select: {
id: true,
email: true,
name: true,
avatar: true,
createdAt: true,
},
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
return NextResponse.json(user);
} catch (error) {
console.error('Erro no GET /api/users/[id]:', error);
return NextResponse.json({ error: 'Error fetching user' }, { status: 500 });
}
}
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params;
const data = await request.json();
// Verificar se email já está em uso por outro usuário
if (data.email) {
const existingUser = await prisma.user.findFirst({
where: {
email: data.email,
NOT: { id },
},
});
if (existingUser) {
return NextResponse.json({ error: 'Email já está em uso' }, { status: 400 });
}
}
const updateData: any = {
email: data.email,
name: data.name,
};
// Se enviou senha, fazer hash
if (data.password) {
updateData.password = await bcrypt.hash(data.password, 10);
}
const user = await prisma.user.update({
where: { id },
data: updateData,
select: {
id: true,
email: true,
name: true,
avatar: true,
createdAt: true,
},
});
return NextResponse.json(user);
} catch (error) {
return NextResponse.json({ error: 'Error updating user' }, { status: 500 });
}
}
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params;
// Verificar se não é o último usuário
const userCount = await prisma.user.count();
if (userCount <= 1) {
return NextResponse.json({ error: 'Não é possível excluir o último usuário do sistema' }, { status: 400 });
}
await prisma.user.delete({
where: { id },
});
return NextResponse.json({ message: 'User deleted' });
} catch (error) {
return NextResponse.json({ error: 'Error deleting user' }, { status: 500 });
}
}

View File

@@ -0,0 +1,130 @@
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
jwt.verify(token, JWT_SECRET);
// Get form data
const formData = await request.formData();
const file = formData.get('avatar') as File;
const userId = formData.get('userId') as string;
if (!file) {
return NextResponse.json(
{ error: 'Nenhum arquivo enviado' },
{ status: 400 }
);
}
if (!userId) {
return NextResponse.json(
{ error: 'ID do usuário não fornecido' },
{ 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/${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: 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: 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 }
);
}
}

View File

@@ -0,0 +1,65 @@
import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import * as bcrypt from 'bcryptjs';
export async function GET() {
try {
const users = await prisma.user.findMany({
orderBy: { createdAt: 'desc' },
select: {
id: true,
email: true,
name: true,
avatar: true,
createdAt: true,
},
});
return NextResponse.json(users);
} catch (error) {
console.error('Error fetching users:', error);
return NextResponse.json({ error: 'Error fetching users' }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const data = await request.json();
// Validações
if (!data.email || !data.password) {
return NextResponse.json({ error: 'Email e senha são obrigatórios' }, { status: 400 });
}
// Verificar se email já existe
const existingUser = await prisma.user.findUnique({
where: { email: data.email },
});
if (existingUser) {
return NextResponse.json({ error: 'Email já está em uso' }, { status: 400 });
}
// Hash da senha
const hashedPassword = await bcrypt.hash(data.password, 10);
const user = await prisma.user.create({
data: {
email: data.email,
name: data.name,
password: hashedPassword,
},
select: {
id: true,
email: true,
name: true,
avatar: true,
createdAt: true,
},
});
return NextResponse.json(user);
} catch (error) {
console.error('Error creating user:', error);
return NextResponse.json({ error: 'Error creating user' }, { status: 500 });
}
}