Initial commit: HotWives Platform completa
- Backend completo com Express, TypeScript e Prisma - Sistema de autenticação JWT - API REST com todas as funcionalidades - Sistema de mensagens e chat em tempo real (Socket.io) - Upload e gerenciamento de fotos - Sistema de perfis com verificação - Busca avançada com filtros - Sistema de eventos - Dashboard administrativo - Frontend Next.js 14 com TypeScript - Design moderno com Tailwind CSS - Componentes UI com Radix UI - Tema dark/light - Configuração Nginx pronta para produção - Scripts de instalação e deploy - Documentação completa
This commit is contained in:
51
backend/package.json
Normal file
51
backend/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "hotwives-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "API Backend para HotWives Platform",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:studio": "prisma studio",
|
||||
"prisma:push": "prisma db push"
|
||||
},
|
||||
"keywords": ["api", "express", "typescript"],
|
||||
"author": "HotWives Team",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.7.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.7",
|
||||
"sharp": "^0.33.1",
|
||||
"socket.io": "^4.6.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"prisma": "^5.7.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
298
backend/prisma/schema.prisma
Normal file
298
backend/prisma/schema.prisma
Normal file
@@ -0,0 +1,298 @@
|
||||
// Prisma Schema para HotWives Platform
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
USER
|
||||
PREMIUM
|
||||
ADMIN
|
||||
MODERATOR
|
||||
}
|
||||
|
||||
enum Gender {
|
||||
MALE
|
||||
FEMALE
|
||||
COUPLE
|
||||
OTHER
|
||||
}
|
||||
|
||||
enum RelationshipType {
|
||||
SINGLE
|
||||
COUPLE
|
||||
OPEN_RELATIONSHIP
|
||||
COMPLICATED
|
||||
}
|
||||
|
||||
enum VerificationStatus {
|
||||
UNVERIFIED
|
||||
PENDING
|
||||
VERIFIED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum EventStatus {
|
||||
DRAFT
|
||||
PUBLISHED
|
||||
CANCELLED
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
password String
|
||||
role UserRole @default(USER)
|
||||
isActive Boolean @default(true)
|
||||
emailVerified Boolean @default(false)
|
||||
verificationToken String?
|
||||
resetToken String?
|
||||
resetTokenExpiry DateTime?
|
||||
lastLogin DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relacionamentos
|
||||
profile Profile?
|
||||
sentMessages Message[] @relation("SentMessages")
|
||||
receivedMessages Message[] @relation("ReceivedMessages")
|
||||
favoriteUsers Favorite[] @relation("UserFavorites")
|
||||
favoritedBy Favorite[] @relation("FavoritedUser")
|
||||
blockedUsers Block[] @relation("UserBlocks")
|
||||
blockedBy Block[] @relation("BlockedUser")
|
||||
photos Photo[]
|
||||
events Event[]
|
||||
eventParticipants EventParticipant[]
|
||||
reports Report[] @relation("ReportCreator")
|
||||
reportedIn Report[] @relation("ReportedUser")
|
||||
notifications Notification[]
|
||||
subscriptions Subscription[]
|
||||
|
||||
@@index([email])
|
||||
@@index([role])
|
||||
}
|
||||
|
||||
model Profile {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique
|
||||
username String @unique
|
||||
displayName String
|
||||
bio String? @db.Text
|
||||
age Int?
|
||||
gender Gender
|
||||
relationshipType RelationshipType
|
||||
location String?
|
||||
city String?
|
||||
state String?
|
||||
country String @default("Brasil")
|
||||
avatarUrl String?
|
||||
coverUrl String?
|
||||
verificationStatus VerificationStatus @default(UNVERIFIED)
|
||||
verificationPhotoUrl String?
|
||||
|
||||
// Preferências
|
||||
lookingFor String[]
|
||||
interests String[]
|
||||
languages String[]
|
||||
|
||||
// Privacidade
|
||||
showAge Boolean @default(true)
|
||||
showLocation Boolean @default(true)
|
||||
showOnline Boolean @default(true)
|
||||
allowMessages Boolean @default(true)
|
||||
|
||||
// Estatísticas
|
||||
profileViews Int @default(0)
|
||||
photoCount Int @default(0)
|
||||
favoritesCount Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([username])
|
||||
@@index([gender])
|
||||
@@index([city])
|
||||
@@index([verificationStatus])
|
||||
}
|
||||
|
||||
model Photo {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
url String
|
||||
thumbnail String?
|
||||
isPrivate Boolean @default(false)
|
||||
isAvatar Boolean @default(false)
|
||||
isCover Boolean @default(false)
|
||||
caption String?
|
||||
order Int @default(0)
|
||||
likes Int @default(0)
|
||||
views Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([isPrivate])
|
||||
}
|
||||
|
||||
model Message {
|
||||
id String @id @default(uuid())
|
||||
senderId String
|
||||
receiverId String
|
||||
content String @db.Text
|
||||
isRead Boolean @default(false)
|
||||
readAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
sender User @relation("SentMessages", fields: [senderId], references: [id], onDelete: Cascade)
|
||||
receiver User @relation("ReceivedMessages", fields: [receiverId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([senderId])
|
||||
@@index([receiverId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model Favorite {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
favoriteId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation("UserFavorites", fields: [userId], references: [id], onDelete: Cascade)
|
||||
favorite User @relation("FavoritedUser", fields: [favoriteId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, favoriteId])
|
||||
@@index([userId])
|
||||
@@index([favoriteId])
|
||||
}
|
||||
|
||||
model Block {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
blockedId String
|
||||
reason String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation("UserBlocks", fields: [userId], references: [id], onDelete: Cascade)
|
||||
blocked User @relation("BlockedUser", fields: [blockedId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, blockedId])
|
||||
@@index([userId])
|
||||
@@index([blockedId])
|
||||
}
|
||||
|
||||
model Event {
|
||||
id String @id @default(uuid())
|
||||
creatorId String
|
||||
title String
|
||||
description String @db.Text
|
||||
date DateTime
|
||||
endDate DateTime?
|
||||
location String
|
||||
city String
|
||||
state String
|
||||
country String @default("Brasil")
|
||||
address String?
|
||||
coverImage String?
|
||||
maxParticipants Int?
|
||||
isPrivate Boolean @default(false)
|
||||
requiresApproval Boolean @default(true)
|
||||
status EventStatus @default(DRAFT)
|
||||
tags String[]
|
||||
price Float?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||
participants EventParticipant[]
|
||||
|
||||
@@index([creatorId])
|
||||
@@index([date])
|
||||
@@index([city])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model EventParticipant {
|
||||
id String @id @default(uuid())
|
||||
eventId String
|
||||
userId String
|
||||
status String @default("pending") // pending, approved, rejected
|
||||
message String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([eventId, userId])
|
||||
@@index([eventId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Report {
|
||||
id String @id @default(uuid())
|
||||
reporterId String
|
||||
reportedId String
|
||||
reason String
|
||||
description String? @db.Text
|
||||
status String @default("pending") // pending, reviewing, resolved, dismissed
|
||||
resolution String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
reporter User @relation("ReportCreator", fields: [reporterId], references: [id], onDelete: Cascade)
|
||||
reported User @relation("ReportedUser", fields: [reportedId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([reporterId])
|
||||
@@index([reportedId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
type String // message, like, favorite, event, system
|
||||
title String
|
||||
message String @db.Text
|
||||
link String?
|
||||
isRead Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([isRead])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
plan String // premium_monthly, premium_yearly
|
||||
status String @default("active") // active, cancelled, expired
|
||||
startDate DateTime @default(now())
|
||||
endDate DateTime
|
||||
autoRenew Boolean @default(true)
|
||||
paymentMethod String?
|
||||
transactionId String?
|
||||
amount Float
|
||||
currency String @default("BRL")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@index([endDate])
|
||||
}
|
||||
|
||||
314
backend/src/controllers/admin.controller.ts
Normal file
314
backend/src/controllers/admin.controller.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import { Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const getStats = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const [
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
verifiedUsers,
|
||||
totalEvents,
|
||||
totalPhotos,
|
||||
totalMessages,
|
||||
pendingVerifications,
|
||||
pendingReports
|
||||
] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.user.count({ where: { isActive: true } }),
|
||||
prisma.profile.count({ where: { verificationStatus: 'VERIFIED' } }),
|
||||
prisma.event.count(),
|
||||
prisma.photo.count(),
|
||||
prisma.message.count(),
|
||||
prisma.profile.count({ where: { verificationStatus: 'PENDING' } }),
|
||||
prisma.report.count({ where: { status: 'pending' } })
|
||||
]);
|
||||
|
||||
// Usuários registrados nos últimos 30 dias
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
const newUsers = await prisma.user.count({
|
||||
where: {
|
||||
createdAt: { gte: thirtyDaysAgo }
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
verifiedUsers,
|
||||
totalEvents,
|
||||
totalPhotos,
|
||||
totalMessages,
|
||||
newUsers,
|
||||
pendingVerifications,
|
||||
pendingReports
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar estatísticas:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar estatísticas' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getUsers = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { page = 1, limit = 50, search, role, isActive } = req.query;
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
const where: any = {};
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ email: { contains: String(search), mode: 'insensitive' } },
|
||||
{ profile: { username: { contains: String(search), mode: 'insensitive' } } },
|
||||
{ profile: { displayName: { contains: String(search), mode: 'insensitive' } } }
|
||||
];
|
||||
}
|
||||
|
||||
if (role) {
|
||||
where.role = role;
|
||||
}
|
||||
|
||||
if (isActive !== undefined) {
|
||||
where.isActive = isActive === 'true';
|
||||
}
|
||||
|
||||
const [users, total] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: Number(limit),
|
||||
include: {
|
||||
profile: true,
|
||||
subscriptions: {
|
||||
where: { status: 'active' }
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
}),
|
||||
prisma.user.count({ where })
|
||||
]);
|
||||
|
||||
res.json({
|
||||
users,
|
||||
pagination: {
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / Number(limit))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar usuários:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar usuários' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserRole = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { role } = req.body;
|
||||
|
||||
if (!['USER', 'PREMIUM', 'MODERATOR', 'ADMIN'].includes(role)) {
|
||||
return res.status(400).json({ error: 'Role inválido' });
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id },
|
||||
data: { role }
|
||||
});
|
||||
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar role:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar role' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserStatus = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { isActive } = req.body;
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id },
|
||||
data: { isActive }
|
||||
});
|
||||
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar status:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar status' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getPendingVerifications = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const profiles = await prisma.profile.findMany({
|
||||
where: {
|
||||
verificationStatus: 'PENDING'
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
createdAt: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { updatedAt: 'asc' }
|
||||
});
|
||||
|
||||
res.json(profiles);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar verificações:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar verificações' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateVerification = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, reason } = req.body;
|
||||
|
||||
if (!['VERIFIED', 'REJECTED'].includes(status)) {
|
||||
return res.status(400).json({ error: 'Status inválido' });
|
||||
}
|
||||
|
||||
const profile = await prisma.profile.update({
|
||||
where: { id },
|
||||
data: { verificationStatus: status }
|
||||
});
|
||||
|
||||
// Notificar usuário
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: profile.userId,
|
||||
type: 'system',
|
||||
title: status === 'VERIFIED' ? 'Perfil Verificado!' : 'Verificação Rejeitada',
|
||||
message: status === 'VERIFIED'
|
||||
? 'Parabéns! Seu perfil foi verificado.'
|
||||
: `Sua verificação foi rejeitada. ${reason || ''}`
|
||||
}
|
||||
});
|
||||
|
||||
res.json(profile);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar verificação:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar verificação' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getReports = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { page = 1, limit = 50, status } = req.query;
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
const where: any = {};
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const [reports, total] = await Promise.all([
|
||||
prisma.report.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: Number(limit),
|
||||
include: {
|
||||
reporter: {
|
||||
include: { profile: true }
|
||||
},
|
||||
reported: {
|
||||
include: { profile: true }
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
}),
|
||||
prisma.report.count({ where })
|
||||
]);
|
||||
|
||||
res.json({
|
||||
reports,
|
||||
pagination: {
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / Number(limit))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar denúncias:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar denúncias' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateReport = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, resolution } = req.body;
|
||||
|
||||
const report = await prisma.report.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status,
|
||||
resolution
|
||||
}
|
||||
});
|
||||
|
||||
// Se resolvido, notificar o denunciante
|
||||
if (status === 'resolved') {
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: report.reporterId,
|
||||
type: 'system',
|
||||
title: 'Denúncia Resolvida',
|
||||
message: 'Sua denúncia foi analisada e resolvida pela equipe.'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.json(report);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar denúncia:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar denúncia' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllEvents = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { page = 1, limit = 50 } = req.query;
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
const [events, total] = await Promise.all([
|
||||
prisma.event.findMany({
|
||||
skip,
|
||||
take: Number(limit),
|
||||
include: {
|
||||
creator: {
|
||||
include: { profile: true }
|
||||
},
|
||||
participants: true
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
}),
|
||||
prisma.event.count()
|
||||
]);
|
||||
|
||||
res.json({
|
||||
events,
|
||||
pagination: {
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / Number(limit))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar eventos:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar eventos' });
|
||||
}
|
||||
};
|
||||
|
||||
259
backend/src/controllers/auth.controller.ts
Normal file
259
backend/src/controllers/auth.controller.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { hashPassword, comparePassword } from '../utils/password.utils';
|
||||
import { generateToken } from '../utils/jwt.utils';
|
||||
import { sendVerificationEmail, sendPasswordResetEmail } from '../utils/email.utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const register = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, password, username, displayName, gender, relationshipType, age, city, state } = req.body;
|
||||
|
||||
// Verificar se o email já existe
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'Email já cadastrado' });
|
||||
}
|
||||
|
||||
// Verificar se o username já existe
|
||||
const existingUsername = await prisma.profile.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (existingUsername) {
|
||||
return res.status(400).json({ error: 'Username já está em uso' });
|
||||
}
|
||||
|
||||
// Hash da senha
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
// Gerar token de verificação
|
||||
const verificationToken = uuidv4();
|
||||
|
||||
// Criar usuário e perfil
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
verificationToken,
|
||||
profile: {
|
||||
create: {
|
||||
username,
|
||||
displayName,
|
||||
gender,
|
||||
relationshipType,
|
||||
age: age || null,
|
||||
city: city || null,
|
||||
state: state || null
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
});
|
||||
|
||||
// Enviar email de verificação
|
||||
try {
|
||||
await sendVerificationEmail(email, verificationToken);
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar email de verificação:', error);
|
||||
}
|
||||
|
||||
// Gerar token JWT
|
||||
const token = generateToken(user.id, user.role);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Usuário criado com sucesso. Verifique seu email.',
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
profile: user.profile
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao registrar usuário:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar usuário' });
|
||||
}
|
||||
};
|
||||
|
||||
export const login = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Buscar usuário
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: { profile: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Credenciais inválidas' });
|
||||
}
|
||||
|
||||
// Verificar senha
|
||||
const isValidPassword = await comparePassword(password, user.password);
|
||||
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ error: 'Credenciais inválidas' });
|
||||
}
|
||||
|
||||
// Verificar se o usuário está ativo
|
||||
if (!user.isActive) {
|
||||
return res.status(403).json({ error: 'Conta desativada' });
|
||||
}
|
||||
|
||||
// Atualizar último login
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLogin: new Date() }
|
||||
});
|
||||
|
||||
// Gerar token
|
||||
const token = generateToken(user.id, user.role);
|
||||
|
||||
res.json({
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
emailVerified: user.emailVerified,
|
||||
profile: user.profile
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer login:', error);
|
||||
res.status(500).json({ error: 'Erro ao fazer login' });
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyEmail = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { token } = req.params;
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { verificationToken: token }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({ error: 'Token inválido' });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
emailVerified: true,
|
||||
verificationToken: null
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ message: 'Email verificado com sucesso' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao verificar email:', error);
|
||||
res.status(500).json({ error: 'Erro ao verificar email' });
|
||||
}
|
||||
};
|
||||
|
||||
export const forgotPassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
// Por segurança, não informar que o email não existe
|
||||
return res.json({ message: 'Se o email existir, um link de recuperação será enviado' });
|
||||
}
|
||||
|
||||
// Gerar token de reset
|
||||
const resetToken = uuidv4();
|
||||
const resetTokenExpiry = new Date(Date.now() + 3600000); // 1 hora
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
resetToken,
|
||||
resetTokenExpiry
|
||||
}
|
||||
});
|
||||
|
||||
// Enviar email
|
||||
try {
|
||||
await sendPasswordResetEmail(email, resetToken);
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar email:', error);
|
||||
}
|
||||
|
||||
res.json({ message: 'Se o email existir, um link de recuperação será enviado' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao solicitar reset de senha:', error);
|
||||
res.status(500).json({ error: 'Erro ao processar solicitação' });
|
||||
}
|
||||
};
|
||||
|
||||
export const resetPassword = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { token, password } = req.body;
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
resetToken: token,
|
||||
resetTokenExpiry: {
|
||||
gte: new Date()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(400).json({ error: 'Token inválido ou expirado' });
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
resetToken: null,
|
||||
resetTokenExpiry: null
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ message: 'Senha redefinida com sucesso' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao redefinir senha:', error);
|
||||
res.status(500).json({ error: 'Erro ao redefinir senha' });
|
||||
}
|
||||
};
|
||||
|
||||
export const refreshToken = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId }
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return res.status(401).json({ error: 'Usuário não encontrado' });
|
||||
}
|
||||
|
||||
const token = generateToken(user.id, user.role);
|
||||
|
||||
res.json({ token });
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar token:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar token' });
|
||||
}
|
||||
};
|
||||
|
||||
357
backend/src/controllers/event.controller.ts
Normal file
357
backend/src/controllers/event.controller.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
import { Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const getEvents = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, upcoming = 'true' } = req.query;
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
const where: any = { status: 'PUBLISHED' };
|
||||
|
||||
if (upcoming === 'true') {
|
||||
where.date = { gte: new Date() };
|
||||
}
|
||||
|
||||
const [events, total] = await Promise.all([
|
||||
prisma.event.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: Number(limit),
|
||||
include: {
|
||||
creator: {
|
||||
include: { profile: true }
|
||||
},
|
||||
participants: {
|
||||
where: { status: 'approved' },
|
||||
include: {
|
||||
user: {
|
||||
include: { profile: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { date: 'asc' }
|
||||
}),
|
||||
prisma.event.count({ where })
|
||||
]);
|
||||
|
||||
res.json({
|
||||
events,
|
||||
pagination: {
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / Number(limit))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar eventos:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar eventos' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getEvent = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const event = await prisma.event.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
creator: {
|
||||
include: { profile: true }
|
||||
},
|
||||
participants: {
|
||||
include: {
|
||||
user: {
|
||||
include: { profile: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ error: 'Evento não encontrado' });
|
||||
}
|
||||
|
||||
// Verificar se é privado e se o usuário tem acesso
|
||||
if (event.isPrivate && event.creatorId !== req.userId) {
|
||||
const isParticipant = event.participants.some(
|
||||
p => p.userId === req.userId && p.status === 'approved'
|
||||
);
|
||||
|
||||
if (!isParticipant) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
}
|
||||
|
||||
res.json(event);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar evento:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar evento' });
|
||||
}
|
||||
};
|
||||
|
||||
export const createEvent = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
date,
|
||||
endDate,
|
||||
location,
|
||||
city,
|
||||
state,
|
||||
address,
|
||||
maxParticipants,
|
||||
isPrivate,
|
||||
requiresApproval,
|
||||
tags,
|
||||
price
|
||||
} = req.body;
|
||||
|
||||
let coverImage = null;
|
||||
if (req.file) {
|
||||
coverImage = `/uploads/photos/${req.file.filename}`;
|
||||
}
|
||||
|
||||
const event = await prisma.event.create({
|
||||
data: {
|
||||
creatorId: req.userId!,
|
||||
title,
|
||||
description,
|
||||
date: new Date(date),
|
||||
endDate: endDate ? new Date(endDate) : null,
|
||||
location,
|
||||
city,
|
||||
state,
|
||||
address,
|
||||
coverImage,
|
||||
maxParticipants: maxParticipants ? Number(maxParticipants) : null,
|
||||
isPrivate: isPrivate === 'true',
|
||||
requiresApproval: requiresApproval !== 'false',
|
||||
tags: tags ? JSON.parse(tags) : [],
|
||||
price: price ? parseFloat(price) : null,
|
||||
status: 'PUBLISHED'
|
||||
},
|
||||
include: {
|
||||
creator: {
|
||||
include: { profile: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(event);
|
||||
} catch (error) {
|
||||
console.error('Erro ao criar evento:', error);
|
||||
res.status(500).json({ error: 'Erro ao criar evento' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateEvent = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const event = await prisma.event.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ error: 'Evento não encontrado' });
|
||||
}
|
||||
|
||||
if (event.creatorId !== req.userId) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
|
||||
const updateData: any = { ...req.body };
|
||||
|
||||
if (req.body.date) {
|
||||
updateData.date = new Date(req.body.date);
|
||||
}
|
||||
|
||||
if (req.body.endDate) {
|
||||
updateData.endDate = new Date(req.body.endDate);
|
||||
}
|
||||
|
||||
if (req.file) {
|
||||
updateData.coverImage = `/uploads/photos/${req.file.filename}`;
|
||||
}
|
||||
|
||||
if (req.body.tags) {
|
||||
updateData.tags = JSON.parse(req.body.tags);
|
||||
}
|
||||
|
||||
const updatedEvent = await prisma.event.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
creator: {
|
||||
include: { profile: true }
|
||||
},
|
||||
participants: true
|
||||
}
|
||||
});
|
||||
|
||||
res.json(updatedEvent);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar evento:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar evento' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteEvent = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const event = await prisma.event.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ error: 'Evento não encontrado' });
|
||||
}
|
||||
|
||||
if (event.creatorId !== req.userId) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
|
||||
await prisma.event.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
res.json({ message: 'Evento deletado com sucesso' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar evento:', error);
|
||||
res.status(500).json({ error: 'Erro ao deletar evento' });
|
||||
}
|
||||
};
|
||||
|
||||
export const joinEvent = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { message } = req.body;
|
||||
|
||||
const event = await prisma.event.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
participants: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ error: 'Evento não encontrado' });
|
||||
}
|
||||
|
||||
// Verificar se já está participando
|
||||
const existing = await prisma.eventParticipant.findUnique({
|
||||
where: {
|
||||
eventId_userId: {
|
||||
eventId: id,
|
||||
userId: req.userId!
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({ error: 'Você já está participando deste evento' });
|
||||
}
|
||||
|
||||
// Verificar limite de participantes
|
||||
if (event.maxParticipants) {
|
||||
const approvedCount = event.participants.filter(p => p.status === 'approved').length;
|
||||
if (approvedCount >= event.maxParticipants) {
|
||||
return res.status(400).json({ error: 'Evento lotado' });
|
||||
}
|
||||
}
|
||||
|
||||
const status = event.requiresApproval ? 'pending' : 'approved';
|
||||
|
||||
const participant = await prisma.eventParticipant.create({
|
||||
data: {
|
||||
eventId: id,
|
||||
userId: req.userId!,
|
||||
status,
|
||||
message
|
||||
}
|
||||
});
|
||||
|
||||
// Notificar criador
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: event.creatorId,
|
||||
type: 'event',
|
||||
title: 'Nova Solicitação de Participação',
|
||||
message: `Alguém quer participar do seu evento: ${event.title}`,
|
||||
link: `/events/${id}`
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(participant);
|
||||
} catch (error) {
|
||||
console.error('Erro ao participar do evento:', error);
|
||||
res.status(500).json({ error: 'Erro ao participar do evento' });
|
||||
}
|
||||
};
|
||||
|
||||
export const leaveEvent = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await prisma.eventParticipant.deleteMany({
|
||||
where: {
|
||||
eventId: id,
|
||||
userId: req.userId
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ message: 'Você saiu do evento' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao sair do evento:', error);
|
||||
res.status(500).json({ error: 'Erro ao sair do evento' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateParticipantStatus = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id, participantId } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
const event = await prisma.event.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
return res.status(404).json({ error: 'Evento não encontrado' });
|
||||
}
|
||||
|
||||
if (event.creatorId !== req.userId) {
|
||||
return res.status(403).json({ error: 'Apenas o criador pode aprovar participantes' });
|
||||
}
|
||||
|
||||
const participant = await prisma.eventParticipant.update({
|
||||
where: { id: participantId },
|
||||
data: { status }
|
||||
});
|
||||
|
||||
// Notificar participante
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: participant.userId,
|
||||
type: 'event',
|
||||
title: status === 'approved' ? 'Participação Aprovada' : 'Participação Rejeitada',
|
||||
message: `Sua participação no evento "${event.title}" foi ${status === 'approved' ? 'aprovada' : 'rejeitada'}`,
|
||||
link: `/events/${id}`
|
||||
}
|
||||
});
|
||||
|
||||
res.json(participant);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar status do participante:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar status' });
|
||||
}
|
||||
};
|
||||
|
||||
232
backend/src/controllers/message.controller.ts
Normal file
232
backend/src/controllers/message.controller.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const getConversations = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
// Buscar mensagens enviadas e recebidas
|
||||
const messages = await prisma.message.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ senderId: req.userId },
|
||||
{ receiverId: req.userId }
|
||||
]
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
sender: {
|
||||
include: { profile: true }
|
||||
},
|
||||
receiver: {
|
||||
include: { profile: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Agrupar por conversas
|
||||
const conversationsMap = new Map();
|
||||
|
||||
messages.forEach(message => {
|
||||
const otherUserId = message.senderId === req.userId
|
||||
? message.receiverId
|
||||
: message.senderId;
|
||||
|
||||
if (!conversationsMap.has(otherUserId)) {
|
||||
const otherUser = message.senderId === req.userId
|
||||
? message.receiver
|
||||
: message.sender;
|
||||
|
||||
const unreadCount = messages.filter(
|
||||
m => m.senderId === otherUserId &&
|
||||
m.receiverId === req.userId &&
|
||||
!m.isRead
|
||||
).length;
|
||||
|
||||
conversationsMap.set(otherUserId, {
|
||||
user: otherUser,
|
||||
lastMessage: message,
|
||||
unreadCount
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const conversations = Array.from(conversationsMap.values());
|
||||
|
||||
res.json(conversations);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar conversas:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar conversas' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getConversation = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { page = 1, limit = 50 } = req.query;
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
const messages = await prisma.message.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ senderId: req.userId, receiverId: userId },
|
||||
{ senderId: userId, receiverId: req.userId }
|
||||
]
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: Number(limit),
|
||||
include: {
|
||||
sender: {
|
||||
include: { profile: true }
|
||||
},
|
||||
receiver: {
|
||||
include: { profile: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Marcar como lidas as mensagens recebidas
|
||||
await prisma.message.updateMany({
|
||||
where: {
|
||||
senderId: userId,
|
||||
receiverId: req.userId,
|
||||
isRead: false
|
||||
},
|
||||
data: { isRead: true, readAt: new Date() }
|
||||
});
|
||||
|
||||
res.json(messages.reverse());
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar conversa:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar conversa' });
|
||||
}
|
||||
};
|
||||
|
||||
export const sendMessage = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { receiverId, content } = req.body;
|
||||
|
||||
if (!receiverId || !content) {
|
||||
return res.status(400).json({ error: 'Destinatário e conteúdo são obrigatórios' });
|
||||
}
|
||||
|
||||
// Verificar se o destinatário existe
|
||||
const receiver = await prisma.user.findUnique({
|
||||
where: { id: receiverId },
|
||||
include: { profile: true }
|
||||
});
|
||||
|
||||
if (!receiver) {
|
||||
return res.status(404).json({ error: 'Destinatário não encontrado' });
|
||||
}
|
||||
|
||||
// Verificar se o usuário está bloqueado
|
||||
const isBlocked = await prisma.block.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId: req.userId, blockedId: receiverId },
|
||||
{ userId: receiverId, blockedId: req.userId }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
if (isBlocked) {
|
||||
return res.status(403).json({ error: 'Não é possível enviar mensagem' });
|
||||
}
|
||||
|
||||
// Verificar configurações de privacidade
|
||||
if (!receiver.profile?.allowMessages) {
|
||||
return res.status(403).json({ error: 'Usuário não aceita mensagens' });
|
||||
}
|
||||
|
||||
const message = await prisma.message.create({
|
||||
data: {
|
||||
senderId: req.userId!,
|
||||
receiverId,
|
||||
content
|
||||
},
|
||||
include: {
|
||||
sender: {
|
||||
include: { profile: true }
|
||||
},
|
||||
receiver: {
|
||||
include: { profile: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Criar notificação
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: receiverId,
|
||||
type: 'message',
|
||||
title: 'Nova Mensagem',
|
||||
message: `${message.sender.profile?.displayName} enviou uma mensagem`,
|
||||
link: `/messages/${req.userId}`
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(message);
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar mensagem:', error);
|
||||
res.status(500).json({ error: 'Erro ao enviar mensagem' });
|
||||
}
|
||||
};
|
||||
|
||||
export const markAsRead = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const message = await prisma.message.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
return res.status(404).json({ error: 'Mensagem não encontrada' });
|
||||
}
|
||||
|
||||
if (message.receiverId !== req.userId) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
|
||||
const updatedMessage = await prisma.message.update({
|
||||
where: { id },
|
||||
data: { isRead: true, readAt: new Date() }
|
||||
});
|
||||
|
||||
res.json(updatedMessage);
|
||||
} catch (error) {
|
||||
console.error('Erro ao marcar mensagem como lida:', error);
|
||||
res.status(500).json({ error: 'Erro ao marcar mensagem' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteMessage = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const message = await prisma.message.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
return res.status(404).json({ error: 'Mensagem não encontrada' });
|
||||
}
|
||||
|
||||
if (message.senderId !== req.userId && message.receiverId !== req.userId) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
|
||||
await prisma.message.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
res.json({ message: 'Mensagem deletada' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar mensagem:', error);
|
||||
res.status(500).json({ error: 'Erro ao deletar mensagem' });
|
||||
}
|
||||
};
|
||||
|
||||
236
backend/src/controllers/photo.controller.ts
Normal file
236
backend/src/controllers/photo.controller.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import { Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { createThumbnail, optimizeImage, deleteFile } from '../utils/image.utils';
|
||||
import path from 'path';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const getUserPhotos = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { userId } = req.query;
|
||||
const targetUserId = userId ? String(userId) : req.userId!;
|
||||
|
||||
const photos = await prisma.photo.findMany({
|
||||
where: {
|
||||
userId: targetUserId,
|
||||
...(targetUserId !== req.userId && { isPrivate: false })
|
||||
},
|
||||
orderBy: { order: 'asc' }
|
||||
});
|
||||
|
||||
res.json(photos);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar fotos:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar fotos' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getPhoto = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const photo = await prisma.photo.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!photo) {
|
||||
return res.status(404).json({ error: 'Foto não encontrada' });
|
||||
}
|
||||
|
||||
// Verificar permissão para fotos privadas
|
||||
if (photo.isPrivate && photo.userId !== req.userId) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
|
||||
// Incrementar visualizações
|
||||
await prisma.photo.update({
|
||||
where: { id },
|
||||
data: { views: { increment: 1 } }
|
||||
});
|
||||
|
||||
res.json(photo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar foto:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar foto' });
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadPhoto = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Nenhum arquivo enviado' });
|
||||
}
|
||||
|
||||
const { caption, isPrivate } = req.body;
|
||||
const fileName = req.file.filename;
|
||||
const url = `/uploads/photos/${fileName}`;
|
||||
|
||||
// Otimizar imagem
|
||||
await optimizeImage(req.file.path);
|
||||
|
||||
// Criar thumbnail
|
||||
const thumbnailPath = await createThumbnail(req.file.path);
|
||||
const thumbnailFileName = path.basename(thumbnailPath);
|
||||
const thumbnailUrl = `/uploads/photos/${thumbnailFileName}`;
|
||||
|
||||
// Buscar o número atual de fotos para definir a ordem
|
||||
const photoCount = await prisma.photo.count({
|
||||
where: { userId: req.userId }
|
||||
});
|
||||
|
||||
const photo = await prisma.photo.create({
|
||||
data: {
|
||||
userId: req.userId!,
|
||||
url,
|
||||
thumbnail: thumbnailUrl,
|
||||
caption,
|
||||
isPrivate: isPrivate === 'true',
|
||||
order: photoCount
|
||||
}
|
||||
});
|
||||
|
||||
// Atualizar contador de fotos no perfil
|
||||
await prisma.profile.update({
|
||||
where: { userId: req.userId },
|
||||
data: { photoCount: { increment: 1 } }
|
||||
});
|
||||
|
||||
res.status(201).json(photo);
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer upload da foto:', error);
|
||||
res.status(500).json({ error: 'Erro ao fazer upload' });
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadMultiplePhotos = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.files || !Array.isArray(req.files) || req.files.length === 0) {
|
||||
return res.status(400).json({ error: 'Nenhum arquivo enviado' });
|
||||
}
|
||||
|
||||
const { isPrivate } = req.body;
|
||||
|
||||
// Buscar o número atual de fotos
|
||||
const photoCount = await prisma.photo.count({
|
||||
where: { userId: req.userId }
|
||||
});
|
||||
|
||||
const photos = [];
|
||||
|
||||
for (let i = 0; i < req.files.length; i++) {
|
||||
const file = req.files[i];
|
||||
const fileName = file.filename;
|
||||
const url = `/uploads/photos/${fileName}`;
|
||||
|
||||
// Otimizar imagem
|
||||
await optimizeImage(file.path);
|
||||
|
||||
// Criar thumbnail
|
||||
const thumbnailPath = await createThumbnail(file.path);
|
||||
const thumbnailFileName = path.basename(thumbnailPath);
|
||||
const thumbnailUrl = `/uploads/photos/${thumbnailFileName}`;
|
||||
|
||||
const photo = await prisma.photo.create({
|
||||
data: {
|
||||
userId: req.userId!,
|
||||
url,
|
||||
thumbnail: thumbnailUrl,
|
||||
isPrivate: isPrivate === 'true',
|
||||
order: photoCount + i
|
||||
}
|
||||
});
|
||||
|
||||
photos.push(photo);
|
||||
}
|
||||
|
||||
// Atualizar contador de fotos no perfil
|
||||
await prisma.profile.update({
|
||||
where: { userId: req.userId },
|
||||
data: { photoCount: { increment: photos.length } }
|
||||
});
|
||||
|
||||
res.status(201).json(photos);
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer upload das fotos:', error);
|
||||
res.status(500).json({ error: 'Erro ao fazer upload' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePhoto = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { caption, isPrivate, order } = req.body;
|
||||
|
||||
const photo = await prisma.photo.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!photo) {
|
||||
return res.status(404).json({ error: 'Foto não encontrada' });
|
||||
}
|
||||
|
||||
if (photo.userId !== req.userId) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
|
||||
const updatedPhoto = await prisma.photo.update({
|
||||
where: { id },
|
||||
data: {
|
||||
caption,
|
||||
isPrivate,
|
||||
order
|
||||
}
|
||||
});
|
||||
|
||||
res.json(updatedPhoto);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar foto:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar foto' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePhoto = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const photo = await prisma.photo.findUnique({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (!photo) {
|
||||
return res.status(404).json({ error: 'Foto não encontrada' });
|
||||
}
|
||||
|
||||
if (photo.userId !== req.userId) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
|
||||
// Deletar arquivos do disco
|
||||
const uploadsDir = path.join(__dirname, '../../uploads/photos');
|
||||
const photoPath = path.join(uploadsDir, path.basename(photo.url));
|
||||
const thumbnailPath = photo.thumbnail ? path.join(uploadsDir, path.basename(photo.thumbnail)) : null;
|
||||
|
||||
deleteFile(photoPath);
|
||||
if (thumbnailPath) {
|
||||
deleteFile(thumbnailPath);
|
||||
}
|
||||
|
||||
// Deletar do banco
|
||||
await prisma.photo.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
// Atualizar contador de fotos no perfil
|
||||
await prisma.profile.update({
|
||||
where: { userId: req.userId },
|
||||
data: { photoCount: { decrement: 1 } }
|
||||
});
|
||||
|
||||
res.json({ message: 'Foto deletada com sucesso' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar foto:', error);
|
||||
res.status(500).json({ error: 'Erro ao deletar foto' });
|
||||
}
|
||||
};
|
||||
|
||||
213
backend/src/controllers/profile.controller.ts
Normal file
213
backend/src/controllers/profile.controller.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
import { createThumbnail, optimizeImage } from '../utils/image.utils';
|
||||
import path from 'path';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const getProfileByUsername = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { username } = req.params;
|
||||
|
||||
const profile = await prisma.profile.findUnique({
|
||||
where: { username },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
lastLogin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
return res.status(404).json({ error: 'Perfil não encontrado' });
|
||||
}
|
||||
|
||||
// Incrementar visualizações se não for o próprio usuário
|
||||
if (req.userId && req.userId !== profile.userId) {
|
||||
await prisma.profile.update({
|
||||
where: { id: profile.id },
|
||||
data: { profileViews: { increment: 1 } }
|
||||
});
|
||||
}
|
||||
|
||||
res.json(profile);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar perfil:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar perfil' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getProfileById = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const profile = await prisma.profile.findFirst({
|
||||
where: { userId: id },
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
lastLogin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!profile) {
|
||||
return res.status(404).json({ error: 'Perfil não encontrado' });
|
||||
}
|
||||
|
||||
res.json(profile);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar perfil:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar perfil' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateProfile = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
displayName,
|
||||
bio,
|
||||
age,
|
||||
city,
|
||||
state,
|
||||
lookingFor,
|
||||
interests,
|
||||
languages,
|
||||
showAge,
|
||||
showLocation,
|
||||
showOnline,
|
||||
allowMessages
|
||||
} = req.body;
|
||||
|
||||
const profile = await prisma.profile.update({
|
||||
where: { userId: req.userId },
|
||||
data: {
|
||||
displayName,
|
||||
bio,
|
||||
age,
|
||||
city,
|
||||
state,
|
||||
lookingFor,
|
||||
interests,
|
||||
languages,
|
||||
showAge,
|
||||
showLocation,
|
||||
showOnline,
|
||||
allowMessages
|
||||
}
|
||||
});
|
||||
|
||||
res.json(profile);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar perfil:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar perfil' });
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadAvatar = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Nenhum arquivo enviado' });
|
||||
}
|
||||
|
||||
const filePath = req.file.path;
|
||||
const fileName = req.file.filename;
|
||||
const url = `/uploads/profiles/${fileName}`;
|
||||
|
||||
// Otimizar imagem
|
||||
await optimizeImage(filePath);
|
||||
|
||||
// Criar thumbnail
|
||||
await createThumbnail(filePath);
|
||||
|
||||
const profile = await prisma.profile.update({
|
||||
where: { userId: req.userId },
|
||||
data: { avatarUrl: url }
|
||||
});
|
||||
|
||||
res.json({ url, profile });
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer upload do avatar:', error);
|
||||
res.status(500).json({ error: 'Erro ao fazer upload' });
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadCover = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Nenhum arquivo enviado' });
|
||||
}
|
||||
|
||||
const fileName = req.file.filename;
|
||||
const url = `/uploads/profiles/${fileName}`;
|
||||
|
||||
// Otimizar imagem
|
||||
await optimizeImage(req.file.path);
|
||||
|
||||
const profile = await prisma.profile.update({
|
||||
where: { userId: req.userId },
|
||||
data: { coverUrl: url }
|
||||
});
|
||||
|
||||
res.json({ url, profile });
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer upload da capa:', error);
|
||||
res.status(500).json({ error: 'Erro ao fazer upload' });
|
||||
}
|
||||
};
|
||||
|
||||
export const submitVerification = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Nenhum arquivo enviado' });
|
||||
}
|
||||
|
||||
const fileName = req.file.filename;
|
||||
const url = `/uploads/verification/${fileName}`;
|
||||
|
||||
const profile = await prisma.profile.update({
|
||||
where: { userId: req.userId },
|
||||
data: {
|
||||
verificationPhotoUrl: url,
|
||||
verificationStatus: 'PENDING'
|
||||
}
|
||||
});
|
||||
|
||||
// Criar notificação para moderadores
|
||||
const admins = await prisma.user.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ role: 'ADMIN' },
|
||||
{ role: 'MODERATOR' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
for (const admin of admins) {
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: admin.id,
|
||||
type: 'system',
|
||||
title: 'Nova Verificação Pendente',
|
||||
message: `Usuário ${profile.username} solicitou verificação de perfil`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ message: 'Verificação enviada para análise', profile });
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar verificação:', error);
|
||||
res.status(500).json({ error: 'Erro ao enviar verificação' });
|
||||
}
|
||||
};
|
||||
|
||||
198
backend/src/controllers/search.controller.ts
Normal file
198
backend/src/controllers/search.controller.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const searchProfiles = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
query,
|
||||
gender,
|
||||
relationshipType,
|
||||
city,
|
||||
state,
|
||||
minAge,
|
||||
maxAge,
|
||||
verified,
|
||||
page = 1,
|
||||
limit = 20
|
||||
} = req.query;
|
||||
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
const where: any = {
|
||||
user: { isActive: true }
|
||||
};
|
||||
|
||||
// Filtro de texto
|
||||
if (query) {
|
||||
where.OR = [
|
||||
{ username: { contains: String(query), mode: 'insensitive' } },
|
||||
{ displayName: { contains: String(query), mode: 'insensitive' } },
|
||||
{ bio: { contains: String(query), mode: 'insensitive' } }
|
||||
];
|
||||
}
|
||||
|
||||
// Filtros específicos
|
||||
if (gender) {
|
||||
where.gender = gender;
|
||||
}
|
||||
|
||||
if (relationshipType) {
|
||||
where.relationshipType = relationshipType;
|
||||
}
|
||||
|
||||
if (city) {
|
||||
where.city = { contains: String(city), mode: 'insensitive' };
|
||||
}
|
||||
|
||||
if (state) {
|
||||
where.state = state;
|
||||
}
|
||||
|
||||
if (minAge || maxAge) {
|
||||
where.age = {};
|
||||
if (minAge) where.age.gte = Number(minAge);
|
||||
if (maxAge) where.age.lte = Number(maxAge);
|
||||
}
|
||||
|
||||
if (verified === 'true') {
|
||||
where.verificationStatus = 'VERIFIED';
|
||||
}
|
||||
|
||||
// Excluir usuários bloqueados
|
||||
if (req.userId) {
|
||||
const blocks = await prisma.block.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId: req.userId },
|
||||
{ blockedId: req.userId }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const blockedIds = blocks.map(b =>
|
||||
b.userId === req.userId ? b.blockedId : b.userId
|
||||
);
|
||||
|
||||
if (blockedIds.length > 0) {
|
||||
where.userId = { notIn: blockedIds };
|
||||
}
|
||||
|
||||
// Excluir o próprio usuário
|
||||
where.userId = { ...where.userId, not: req.userId };
|
||||
}
|
||||
|
||||
const [profiles, total] = await Promise.all([
|
||||
prisma.profile.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: Number(limit),
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
role: true,
|
||||
lastLogin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ verificationStatus: 'desc' },
|
||||
{ user: { lastLogin: 'desc' } }
|
||||
]
|
||||
}),
|
||||
prisma.profile.count({ where })
|
||||
]);
|
||||
|
||||
res.json({
|
||||
profiles,
|
||||
pagination: {
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / Number(limit))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar perfis:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar perfis' });
|
||||
}
|
||||
};
|
||||
|
||||
export const searchEvents = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
query,
|
||||
city,
|
||||
state,
|
||||
startDate,
|
||||
endDate,
|
||||
page = 1,
|
||||
limit = 20
|
||||
} = req.query;
|
||||
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
const where: any = {
|
||||
status: 'PUBLISHED',
|
||||
date: { gte: new Date() }
|
||||
};
|
||||
|
||||
if (query) {
|
||||
where.OR = [
|
||||
{ title: { contains: String(query), mode: 'insensitive' } },
|
||||
{ description: { contains: String(query), mode: 'insensitive' } }
|
||||
];
|
||||
}
|
||||
|
||||
if (city) {
|
||||
where.city = { contains: String(city), mode: 'insensitive' };
|
||||
}
|
||||
|
||||
if (state) {
|
||||
where.state = state;
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
where.date = { ...where.date, gte: new Date(String(startDate)) };
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
where.date = { ...where.date, lte: new Date(String(endDate)) };
|
||||
}
|
||||
|
||||
const [events, total] = await Promise.all([
|
||||
prisma.event.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: Number(limit),
|
||||
include: {
|
||||
creator: {
|
||||
include: { profile: true }
|
||||
},
|
||||
participants: {
|
||||
where: { status: 'approved' }
|
||||
}
|
||||
},
|
||||
orderBy: { date: 'asc' }
|
||||
}),
|
||||
prisma.event.count({ where })
|
||||
]);
|
||||
|
||||
res.json({
|
||||
events,
|
||||
pagination: {
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / Number(limit))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar eventos:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar eventos' });
|
||||
}
|
||||
};
|
||||
|
||||
306
backend/src/controllers/user.controller.ts
Normal file
306
backend/src/controllers/user.controller.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import { Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { AuthRequest } from '../middleware/auth.middleware';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const getCurrentUser = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.userId },
|
||||
include: {
|
||||
profile: true,
|
||||
subscriptions: {
|
||||
where: {
|
||||
status: 'active',
|
||||
endDate: { gte: new Date() }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'Usuário não encontrado' });
|
||||
}
|
||||
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar usuário:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar usuário' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCurrentUser = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { email, currentPassword, newPassword } = req.body;
|
||||
const updateData: any = {};
|
||||
|
||||
if (email) {
|
||||
const existingEmail = await prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
NOT: { id: req.userId }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({ error: 'Email já está em uso' });
|
||||
}
|
||||
|
||||
updateData.email = email;
|
||||
updateData.emailVerified = false;
|
||||
}
|
||||
|
||||
if (newPassword && currentPassword) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.userId }
|
||||
});
|
||||
|
||||
const { comparePassword } = await import('../utils/password.utils');
|
||||
const isValid = await comparePassword(currentPassword, user!.password);
|
||||
|
||||
if (!isValid) {
|
||||
return res.status(400).json({ error: 'Senha atual incorreta' });
|
||||
}
|
||||
|
||||
const { hashPassword } = await import('../utils/password.utils');
|
||||
updateData.password = await hashPassword(newPassword);
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: req.userId },
|
||||
data: updateData,
|
||||
include: { profile: true }
|
||||
});
|
||||
|
||||
res.json(updatedUser);
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar usuário:', error);
|
||||
res.status(500).json({ error: 'Erro ao atualizar usuário' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAccount = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id: req.userId },
|
||||
data: { isActive: false }
|
||||
});
|
||||
|
||||
res.json({ message: 'Conta desativada com sucesso' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar conta:', error);
|
||||
res.status(500).json({ error: 'Erro ao deletar conta' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getNotifications = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { page = 1, limit = 20 } = req.query;
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
const [notifications, total] = await Promise.all([
|
||||
prisma.notification.findMany({
|
||||
where: { userId: req.userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: Number(limit)
|
||||
}),
|
||||
prisma.notification.count({
|
||||
where: { userId: req.userId }
|
||||
})
|
||||
]);
|
||||
|
||||
res.json({
|
||||
notifications,
|
||||
pagination: {
|
||||
page: Number(page),
|
||||
limit: Number(limit),
|
||||
total,
|
||||
pages: Math.ceil(total / Number(limit))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar notificações:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar notificações' });
|
||||
}
|
||||
};
|
||||
|
||||
export const markNotificationAsRead = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const notification = await prisma.notification.updateMany({
|
||||
where: {
|
||||
id,
|
||||
userId: req.userId
|
||||
},
|
||||
data: { isRead: true }
|
||||
});
|
||||
|
||||
if (notification.count === 0) {
|
||||
return res.status(404).json({ error: 'Notificação não encontrada' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Notificação marcada como lida' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao marcar notificação:', error);
|
||||
res.status(500).json({ error: 'Erro ao marcar notificação' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteNotification = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const notification = await prisma.notification.deleteMany({
|
||||
where: {
|
||||
id,
|
||||
userId: req.userId
|
||||
}
|
||||
});
|
||||
|
||||
if (notification.count === 0) {
|
||||
return res.status(404).json({ error: 'Notificação não encontrada' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Notificação deletada' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar notificação:', error);
|
||||
res.status(500).json({ error: 'Erro ao deletar notificação' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getFavorites = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const favorites = await prisma.favorite.findMany({
|
||||
where: { userId: req.userId },
|
||||
include: {
|
||||
favorite: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
res.json(favorites);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar favoritos:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar favoritos' });
|
||||
}
|
||||
};
|
||||
|
||||
export const addFavorite = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
|
||||
if (userId === req.userId) {
|
||||
return res.status(400).json({ error: 'Você não pode favoritar a si mesmo' });
|
||||
}
|
||||
|
||||
const favorite = await prisma.favorite.create({
|
||||
data: {
|
||||
userId: req.userId!,
|
||||
favoriteId: userId
|
||||
}
|
||||
});
|
||||
|
||||
// Criar notificação
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId,
|
||||
type: 'favorite',
|
||||
title: 'Novo Favorito',
|
||||
message: 'Alguém adicionou você aos favoritos!'
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(favorite);
|
||||
} catch (error) {
|
||||
console.error('Erro ao adicionar favorito:', error);
|
||||
res.status(500).json({ error: 'Erro ao adicionar favorito' });
|
||||
}
|
||||
};
|
||||
|
||||
export const removeFavorite = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
|
||||
await prisma.favorite.deleteMany({
|
||||
where: {
|
||||
userId: req.userId,
|
||||
favoriteId: userId
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ message: 'Favorito removido' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao remover favorito:', error);
|
||||
res.status(500).json({ error: 'Erro ao remover favorito' });
|
||||
}
|
||||
};
|
||||
|
||||
export const getBlocks = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const blocks = await prisma.block.findMany({
|
||||
where: { userId: req.userId },
|
||||
include: {
|
||||
blocked: {
|
||||
include: {
|
||||
profile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.json(blocks);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar bloqueios:', error);
|
||||
res.status(500).json({ error: 'Erro ao buscar bloqueios' });
|
||||
}
|
||||
};
|
||||
|
||||
export const blockUser = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { reason } = req.body;
|
||||
|
||||
if (userId === req.userId) {
|
||||
return res.status(400).json({ error: 'Você não pode bloquear a si mesmo' });
|
||||
}
|
||||
|
||||
const block = await prisma.block.create({
|
||||
data: {
|
||||
userId: req.userId!,
|
||||
blockedId: userId,
|
||||
reason
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(block);
|
||||
} catch (error) {
|
||||
console.error('Erro ao bloquear usuário:', error);
|
||||
res.status(500).json({ error: 'Erro ao bloquear usuário' });
|
||||
}
|
||||
};
|
||||
|
||||
export const unblockUser = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
|
||||
await prisma.block.deleteMany({
|
||||
where: {
|
||||
userId: req.userId,
|
||||
blockedId: userId
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ message: 'Usuário desbloqueado' });
|
||||
} catch (error) {
|
||||
console.error('Erro ao desbloquear usuário:', error);
|
||||
res.status(500).json({ error: 'Erro ao desbloquear usuário' });
|
||||
}
|
||||
};
|
||||
|
||||
76
backend/src/middleware/auth.middleware.ts
Normal file
76
backend/src/middleware/auth.middleware.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
userId?: string;
|
||||
userRole?: string;
|
||||
}
|
||||
|
||||
export const authenticate = async (
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Token não fornecido' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
|
||||
userId: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
// Verificar se o usuário existe e está ativo
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: decoded.userId }
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return res.status(401).json({ error: 'Usuário não encontrado ou inativo' });
|
||||
}
|
||||
|
||||
req.userId = decoded.userId;
|
||||
req.userRole = decoded.role;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: 'Token inválido' });
|
||||
}
|
||||
};
|
||||
|
||||
export const authorize = (...roles: string[]) => {
|
||||
return (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
if (!req.userRole || !roles.includes(req.userRole)) {
|
||||
return res.status(403).json({ error: 'Acesso negado' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
export const optionalAuth = async (
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (token) {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
|
||||
userId: string;
|
||||
role: string;
|
||||
};
|
||||
req.userId = decoded.userId;
|
||||
req.userRole = decoded.role;
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
59
backend/src/middleware/upload.middleware.ts
Normal file
59
backend/src/middleware/upload.middleware.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs';
|
||||
|
||||
// Criar diretório de uploads se não existir
|
||||
const uploadDir = path.join(__dirname, '../../uploads');
|
||||
const profileDir = path.join(uploadDir, 'profiles');
|
||||
const photosDir = path.join(uploadDir, 'photos');
|
||||
const verificationDir = path.join(uploadDir, 'verification');
|
||||
|
||||
[uploadDir, profileDir, photosDir, verificationDir].forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Configuração de armazenamento
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
let folder = 'photos';
|
||||
if (req.path.includes('avatar') || req.path.includes('cover')) {
|
||||
folder = 'profiles';
|
||||
} else if (req.path.includes('verification')) {
|
||||
folder = 'verification';
|
||||
}
|
||||
cb(null, path.join(uploadDir, folder));
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueName = `${uuidv4()}${path.extname(file.originalname)}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
|
||||
// Filtro de arquivos - apenas imagens
|
||||
const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
|
||||
const allowedTypes = /jpeg|jpg|png|gif|webp/;
|
||||
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
|
||||
const mimetype = allowedTypes.test(file.mimetype);
|
||||
|
||||
if (mimetype && extname) {
|
||||
return cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Apenas imagens são permitidas (jpeg, jpg, png, gif, webp)'));
|
||||
}
|
||||
};
|
||||
|
||||
// Configuração do multer
|
||||
export const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: parseInt(process.env.MAX_FILE_SIZE || '10485760') // 10MB padrão
|
||||
},
|
||||
fileFilter: fileFilter
|
||||
});
|
||||
|
||||
export const uploadSingle = upload.single('photo');
|
||||
export const uploadMultiple = upload.array('photos', 10);
|
||||
|
||||
19
backend/src/middleware/validation.middleware.ts
Normal file
19
backend/src/middleware/validation.middleware.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { validationResult, ValidationChain } from 'express-validator';
|
||||
|
||||
export const validate = (validations: ValidationChain[]) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
await Promise.all(validations.map(validation => validation.run(req)));
|
||||
|
||||
const errors = validationResult(req);
|
||||
if (errors.isEmpty()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.status(400).json({
|
||||
error: 'Erro de validação',
|
||||
details: errors.array()
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
31
backend/src/routes/admin.routes.ts
Normal file
31
backend/src/routes/admin.routes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate, authorize } from '../middleware/auth.middleware';
|
||||
import * as adminController from '../controllers/admin.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Todas as rotas requerem autenticação de admin/moderador
|
||||
router.use(authenticate);
|
||||
router.use(authorize('ADMIN', 'MODERATOR'));
|
||||
|
||||
// Estatísticas
|
||||
router.get('/stats', adminController.getStats);
|
||||
|
||||
// Gerenciamento de usuários
|
||||
router.get('/users', adminController.getUsers);
|
||||
router.put('/users/:id/role', adminController.updateUserRole);
|
||||
router.put('/users/:id/status', adminController.updateUserStatus);
|
||||
|
||||
// Verificações pendentes
|
||||
router.get('/verifications', adminController.getPendingVerifications);
|
||||
router.put('/verifications/:id', adminController.updateVerification);
|
||||
|
||||
// Denúncias
|
||||
router.get('/reports', adminController.getReports);
|
||||
router.put('/reports/:id', adminController.updateReport);
|
||||
|
||||
// Eventos
|
||||
router.get('/events', adminController.getAllEvents);
|
||||
|
||||
export default router;
|
||||
|
||||
68
backend/src/routes/auth.routes.ts
Normal file
68
backend/src/routes/auth.routes.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Router } from 'express';
|
||||
import { body } from 'express-validator';
|
||||
import { validate } from '../middleware/validation.middleware';
|
||||
import * as authController from '../controllers/auth.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Registro
|
||||
router.post(
|
||||
'/register',
|
||||
validate([
|
||||
body('email').isEmail().withMessage('Email inválido'),
|
||||
body('password')
|
||||
.isLength({ min: 6 })
|
||||
.withMessage('Senha deve ter no mínimo 6 caracteres'),
|
||||
body('username')
|
||||
.isLength({ min: 3, max: 20 })
|
||||
.withMessage('Username deve ter entre 3 e 20 caracteres')
|
||||
.matches(/^[a-zA-Z0-9_]+$/)
|
||||
.withMessage('Username deve conter apenas letras, números e underscore'),
|
||||
body('displayName')
|
||||
.notEmpty()
|
||||
.withMessage('Nome de exibição é obrigatório'),
|
||||
body('gender').isIn(['MALE', 'FEMALE', 'COUPLE', 'OTHER']),
|
||||
body('relationshipType').isIn(['SINGLE', 'COUPLE', 'OPEN_RELATIONSHIP', 'COMPLICATED'])
|
||||
]),
|
||||
authController.register
|
||||
);
|
||||
|
||||
// Login
|
||||
router.post(
|
||||
'/login',
|
||||
validate([
|
||||
body('email').isEmail().withMessage('Email inválido'),
|
||||
body('password').notEmpty().withMessage('Senha é obrigatória')
|
||||
]),
|
||||
authController.login
|
||||
);
|
||||
|
||||
// Verificar email
|
||||
router.get('/verify-email/:token', authController.verifyEmail);
|
||||
|
||||
// Solicitar reset de senha
|
||||
router.post(
|
||||
'/forgot-password',
|
||||
validate([
|
||||
body('email').isEmail().withMessage('Email inválido')
|
||||
]),
|
||||
authController.forgotPassword
|
||||
);
|
||||
|
||||
// Reset de senha
|
||||
router.post(
|
||||
'/reset-password',
|
||||
validate([
|
||||
body('token').notEmpty().withMessage('Token é obrigatório'),
|
||||
body('password')
|
||||
.isLength({ min: 6 })
|
||||
.withMessage('Senha deve ter no mínimo 6 caracteres')
|
||||
]),
|
||||
authController.resetPassword
|
||||
);
|
||||
|
||||
// Refresh token
|
||||
router.post('/refresh-token', authController.refreshToken);
|
||||
|
||||
export default router;
|
||||
|
||||
38
backend/src/routes/event.routes.ts
Normal file
38
backend/src/routes/event.routes.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Router } from 'express';
|
||||
import { body } from 'express-validator';
|
||||
import { authenticate, optionalAuth } from '../middleware/auth.middleware';
|
||||
import { validate } from '../middleware/validation.middleware';
|
||||
import { uploadSingle } from '../middleware/upload.middleware';
|
||||
import * as eventController from '../controllers/event.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Rotas públicas
|
||||
router.get('/', optionalAuth, eventController.getEvents);
|
||||
router.get('/:id', optionalAuth, eventController.getEvent);
|
||||
|
||||
// Rotas autenticadas
|
||||
router.use(authenticate);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
uploadSingle,
|
||||
validate([
|
||||
body('title').notEmpty().withMessage('Título é obrigatório'),
|
||||
body('description').notEmpty().withMessage('Descrição é obrigatória'),
|
||||
body('date').isISO8601().withMessage('Data inválida'),
|
||||
body('location').notEmpty().withMessage('Local é obrigatório'),
|
||||
body('city').notEmpty().withMessage('Cidade é obrigatória'),
|
||||
body('state').notEmpty().withMessage('Estado é obrigatório')
|
||||
]),
|
||||
eventController.createEvent
|
||||
);
|
||||
|
||||
router.put('/:id', uploadSingle, eventController.updateEvent);
|
||||
router.delete('/:id', eventController.deleteEvent);
|
||||
router.post('/:id/join', eventController.joinEvent);
|
||||
router.post('/:id/leave', eventController.leaveEvent);
|
||||
router.put('/:id/participants/:participantId', eventController.updateParticipantStatus);
|
||||
|
||||
export default router;
|
||||
|
||||
17
backend/src/routes/message.routes.ts
Normal file
17
backend/src/routes/message.routes.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import * as messageController from '../controllers/message.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Todas as rotas requerem autenticação
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/conversations', messageController.getConversations);
|
||||
router.get('/conversation/:userId', messageController.getConversation);
|
||||
router.post('/send', messageController.sendMessage);
|
||||
router.put('/:id/read', messageController.markAsRead);
|
||||
router.delete('/:id', messageController.deleteMessage);
|
||||
|
||||
export default router;
|
||||
|
||||
19
backend/src/routes/photo.routes.ts
Normal file
19
backend/src/routes/photo.routes.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import { uploadMultiple, uploadSingle } from '../middleware/upload.middleware';
|
||||
import * as photoController from '../controllers/photo.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Todas as rotas requerem autenticação
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/', photoController.getUserPhotos);
|
||||
router.get('/:id', photoController.getPhoto);
|
||||
router.post('/', uploadSingle, photoController.uploadPhoto);
|
||||
router.post('/multiple', uploadMultiple, photoController.uploadMultiplePhotos);
|
||||
router.put('/:id', photoController.updatePhoto);
|
||||
router.delete('/:id', photoController.deletePhoto);
|
||||
|
||||
export default router;
|
||||
|
||||
34
backend/src/routes/profile.routes.ts
Normal file
34
backend/src/routes/profile.routes.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Router } from 'express';
|
||||
import { body } from 'express-validator';
|
||||
import { authenticate, optionalAuth } from '../middleware/auth.middleware';
|
||||
import { validate } from '../middleware/validation.middleware';
|
||||
import { uploadSingle } from '../middleware/upload.middleware';
|
||||
import * as profileController from '../controllers/profile.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Rotas públicas (com autenticação opcional)
|
||||
router.get('/:username', optionalAuth, profileController.getProfileByUsername);
|
||||
router.get('/id/:id', optionalAuth, profileController.getProfileById);
|
||||
|
||||
// Rotas autenticadas
|
||||
router.use(authenticate);
|
||||
|
||||
router.put(
|
||||
'/me',
|
||||
validate([
|
||||
body('displayName').optional().notEmpty(),
|
||||
body('bio').optional(),
|
||||
body('age').optional().isInt({ min: 18, max: 120 }),
|
||||
body('city').optional(),
|
||||
body('state').optional()
|
||||
]),
|
||||
profileController.updateProfile
|
||||
);
|
||||
|
||||
router.post('/me/avatar', uploadSingle, profileController.uploadAvatar);
|
||||
router.post('/me/cover', uploadSingle, profileController.uploadCover);
|
||||
router.post('/me/verification', uploadSingle, profileController.submitVerification);
|
||||
|
||||
export default router;
|
||||
|
||||
11
backend/src/routes/search.routes.ts
Normal file
11
backend/src/routes/search.routes.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import { optionalAuth } from '../middleware/auth.middleware';
|
||||
import * as searchController from '../controllers/search.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/profiles', optionalAuth, searchController.searchProfiles);
|
||||
router.get('/events', optionalAuth, searchController.searchEvents);
|
||||
|
||||
export default router;
|
||||
|
||||
31
backend/src/routes/user.routes.ts
Normal file
31
backend/src/routes/user.routes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
import * as userController from '../controllers/user.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Todas as rotas requerem autenticação
|
||||
router.use(authenticate);
|
||||
|
||||
// Perfil do usuário atual
|
||||
router.get('/me', userController.getCurrentUser);
|
||||
router.put('/me', userController.updateCurrentUser);
|
||||
router.delete('/me', userController.deleteAccount);
|
||||
|
||||
// Notificações
|
||||
router.get('/notifications', userController.getNotifications);
|
||||
router.put('/notifications/:id/read', userController.markNotificationAsRead);
|
||||
router.delete('/notifications/:id', userController.deleteNotification);
|
||||
|
||||
// Favoritos
|
||||
router.get('/favorites', userController.getFavorites);
|
||||
router.post('/favorites/:userId', userController.addFavorite);
|
||||
router.delete('/favorites/:userId', userController.removeFavorite);
|
||||
|
||||
// Bloqueios
|
||||
router.get('/blocks', userController.getBlocks);
|
||||
router.post('/blocks/:userId', userController.blockUser);
|
||||
router.delete('/blocks/:userId', userController.unblockUser);
|
||||
|
||||
export default router;
|
||||
|
||||
106
backend/src/server.ts
Normal file
106
backend/src/server.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import express, { Express, Request, Response, NextFunction } from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import compression from 'compression';
|
||||
import dotenv from 'dotenv';
|
||||
import { createServer } from 'http';
|
||||
import { Server as SocketIOServer } from 'socket.io';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import path from 'path';
|
||||
|
||||
// Carregar variáveis de ambiente
|
||||
dotenv.config();
|
||||
|
||||
// Importar rotas
|
||||
import authRoutes from './routes/auth.routes';
|
||||
import userRoutes from './routes/user.routes';
|
||||
import profileRoutes from './routes/profile.routes';
|
||||
import photoRoutes from './routes/photo.routes';
|
||||
import messageRoutes from './routes/message.routes';
|
||||
import eventRoutes from './routes/event.routes';
|
||||
import searchRoutes from './routes/search.routes';
|
||||
import adminRoutes from './routes/admin.routes';
|
||||
|
||||
// Importar socket handlers
|
||||
import { setupSocketHandlers } from './sockets/chat.socket';
|
||||
|
||||
const app: Express = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new SocketIOServer(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.FRONTEND_URL || '*',
|
||||
methods: ['GET', 'POST']
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// Middlewares de segurança
|
||||
app.use(helmet({
|
||||
crossOriginResourcePolicy: { policy: "cross-origin" }
|
||||
}));
|
||||
|
||||
// CORS
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || '*',
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Compressão
|
||||
app.use(compression());
|
||||
|
||||
// Body parser
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutos
|
||||
max: 100 // limite de 100 requisições por IP
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// Servir arquivos estáticos (uploads)
|
||||
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req: Request, res: Response) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Rotas da API
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/profiles', profileRoutes);
|
||||
app.use('/api/photos', photoRoutes);
|
||||
app.use('/api/messages', messageRoutes);
|
||||
app.use('/api/events', eventRoutes);
|
||||
app.use('/api/search', searchRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
|
||||
// Configurar Socket.IO
|
||||
setupSocketHandlers(io);
|
||||
|
||||
// Tratamento de erros
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
console.error('Error:', err);
|
||||
res.status(500).json({
|
||||
error: 'Erro interno do servidor',
|
||||
message: process.env.NODE_ENV === 'development' ? err.message : undefined
|
||||
});
|
||||
});
|
||||
|
||||
// Rota 404
|
||||
app.use('*', (req: Request, res: Response) => {
|
||||
res.status(404).json({ error: 'Rota não encontrada' });
|
||||
});
|
||||
|
||||
// Iniciar servidor
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`🚀 Servidor rodando na porta ${PORT}`);
|
||||
console.log(`📡 API disponível em http://localhost:${PORT}/api`);
|
||||
console.log(`🔌 Socket.IO disponível em http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
export { app, io };
|
||||
|
||||
163
backend/src/sockets/chat.socket.ts
Normal file
163
backend/src/sockets/chat.socket.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { Server as SocketIOServer, Socket } from 'socket.io';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface AuthenticatedSocket extends Socket {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
const onlineUsers = new Map<string, string>(); // userId -> socketId
|
||||
|
||||
export const setupSocketHandlers = (io: SocketIOServer) => {
|
||||
// Middleware de autenticação
|
||||
io.use((socket: AuthenticatedSocket, next) => {
|
||||
const token = socket.handshake.auth.token;
|
||||
|
||||
if (!token) {
|
||||
return next(new Error('Authentication error'));
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
|
||||
userId: string;
|
||||
};
|
||||
socket.userId = decoded.userId;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(new Error('Authentication error'));
|
||||
}
|
||||
});
|
||||
|
||||
io.on('connection', (socket: AuthenticatedSocket) => {
|
||||
console.log(`User connected: ${socket.userId}`);
|
||||
|
||||
// Adicionar à lista de usuários online
|
||||
if (socket.userId) {
|
||||
onlineUsers.set(socket.userId, socket.id);
|
||||
io.emit('user:online', { userId: socket.userId });
|
||||
}
|
||||
|
||||
// Entrar em sala de conversa
|
||||
socket.on('conversation:join', (userId: string) => {
|
||||
const roomId = [socket.userId, userId].sort().join('-');
|
||||
socket.join(roomId);
|
||||
console.log(`User ${socket.userId} joined room ${roomId}`);
|
||||
});
|
||||
|
||||
// Sair de sala de conversa
|
||||
socket.on('conversation:leave', (userId: string) => {
|
||||
const roomId = [socket.userId, userId].sort().join('-');
|
||||
socket.leave(roomId);
|
||||
console.log(`User ${socket.userId} left room ${roomId}`);
|
||||
});
|
||||
|
||||
// Enviar mensagem
|
||||
socket.on('message:send', async (data: {
|
||||
receiverId: string;
|
||||
content: string;
|
||||
}) => {
|
||||
try {
|
||||
if (!socket.userId) return;
|
||||
|
||||
// Salvar mensagem no banco
|
||||
const message = await prisma.message.create({
|
||||
data: {
|
||||
senderId: socket.userId,
|
||||
receiverId: data.receiverId,
|
||||
content: data.content
|
||||
},
|
||||
include: {
|
||||
sender: {
|
||||
include: { profile: true }
|
||||
},
|
||||
receiver: {
|
||||
include: { profile: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Enviar para a sala
|
||||
const roomId = [socket.userId, data.receiverId].sort().join('-');
|
||||
io.to(roomId).emit('message:received', message);
|
||||
|
||||
// Se o destinatário estiver online mas não na sala, enviar notificação
|
||||
const receiverSocketId = onlineUsers.get(data.receiverId);
|
||||
if (receiverSocketId) {
|
||||
io.to(receiverSocketId).emit('notification:new', {
|
||||
type: 'message',
|
||||
message: `Nova mensagem de ${message.sender.profile?.displayName}`,
|
||||
data: message
|
||||
});
|
||||
}
|
||||
|
||||
// Criar notificação no banco
|
||||
await prisma.notification.create({
|
||||
data: {
|
||||
userId: data.receiverId,
|
||||
type: 'message',
|
||||
title: 'Nova Mensagem',
|
||||
message: `${message.sender.profile?.displayName} enviou uma mensagem`,
|
||||
link: `/messages/${socket.userId}`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
socket.emit('message:error', { error: 'Failed to send message' });
|
||||
}
|
||||
});
|
||||
|
||||
// Digitando...
|
||||
socket.on('typing:start', (userId: string) => {
|
||||
const receiverSocketId = onlineUsers.get(userId);
|
||||
if (receiverSocketId) {
|
||||
io.to(receiverSocketId).emit('typing:start', { userId: socket.userId });
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('typing:stop', (userId: string) => {
|
||||
const receiverSocketId = onlineUsers.get(userId);
|
||||
if (receiverSocketId) {
|
||||
io.to(receiverSocketId).emit('typing:stop', { userId: socket.userId });
|
||||
}
|
||||
});
|
||||
|
||||
// Marcar mensagem como lida
|
||||
socket.on('message:read', async (messageId: string) => {
|
||||
try {
|
||||
await prisma.message.update({
|
||||
where: { id: messageId },
|
||||
data: { isRead: true, readAt: new Date() }
|
||||
});
|
||||
|
||||
const message = await prisma.message.findUnique({
|
||||
where: { id: messageId }
|
||||
});
|
||||
|
||||
if (message) {
|
||||
const senderSocketId = onlineUsers.get(message.senderId);
|
||||
if (senderSocketId) {
|
||||
io.to(senderSocketId).emit('message:read', { messageId });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking message as read:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Desconexão
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`User disconnected: ${socket.userId}`);
|
||||
if (socket.userId) {
|
||||
onlineUsers.delete(socket.userId);
|
||||
io.emit('user:offline', { userId: socket.userId });
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getOnlineUsers = () => {
|
||||
return Array.from(onlineUsers.keys());
|
||||
};
|
||||
|
||||
82
backend/src/utils/email.utils.ts
Normal file
82
backend/src/utils/email.utils.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: parseInt(process.env.SMTP_PORT || '587'),
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
}
|
||||
});
|
||||
|
||||
interface EmailOptions {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
export const sendEmail = async (options: EmailOptions): Promise<void> => {
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: process.env.EMAIL_FROM || 'noreply@hotwives.com.br',
|
||||
to: options.to,
|
||||
subject: options.subject,
|
||||
html: options.html
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar email:', error);
|
||||
throw new Error('Falha ao enviar email');
|
||||
}
|
||||
};
|
||||
|
||||
export const sendVerificationEmail = async (email: string, token: string): Promise<void> => {
|
||||
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${token}`;
|
||||
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #e91e63;">Bem-vindo ao HotWives!</h2>
|
||||
<p>Obrigado por se cadastrar. Por favor, clique no botão abaixo para verificar seu email:</p>
|
||||
<a href="${verificationUrl}" style="display: inline-block; padding: 12px 24px; background-color: #e91e63; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0;">
|
||||
Verificar Email
|
||||
</a>
|
||||
<p>Ou copie e cole este link no seu navegador:</p>
|
||||
<p style="color: #666; word-break: break-all;">${verificationUrl}</p>
|
||||
<p style="color: #999; font-size: 12px; margin-top: 30px;">
|
||||
Se você não se cadastrou no HotWives, ignore este email.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Verificação de Email - HotWives',
|
||||
html
|
||||
});
|
||||
};
|
||||
|
||||
export const sendPasswordResetEmail = async (email: string, token: string): Promise<void> => {
|
||||
const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
|
||||
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #e91e63;">Redefinir Senha</h2>
|
||||
<p>Você solicitou a redefinição de senha. Clique no botão abaixo para criar uma nova senha:</p>
|
||||
<a href="${resetUrl}" style="display: inline-block; padding: 12px 24px; background-color: #e91e63; color: white; text-decoration: none; border-radius: 5px; margin: 20px 0;">
|
||||
Redefinir Senha
|
||||
</a>
|
||||
<p>Ou copie e cole este link no seu navegador:</p>
|
||||
<p style="color: #666; word-break: break-all;">${resetUrl}</p>
|
||||
<p style="color: #999; font-size: 12px; margin-top: 30px;">
|
||||
Este link expira em 1 hora. Se você não solicitou esta redefinição, ignore este email.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Redefinição de Senha - HotWives',
|
||||
html
|
||||
});
|
||||
};
|
||||
|
||||
45
backend/src/utils/image.utils.ts
Normal file
45
backend/src/utils/image.utils.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import sharp from 'sharp';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
export const resizeImage = async (
|
||||
filePath: string,
|
||||
width: number,
|
||||
height?: number
|
||||
): Promise<string> => {
|
||||
const ext = path.extname(filePath);
|
||||
const fileName = path.basename(filePath, ext);
|
||||
const dir = path.dirname(filePath);
|
||||
const outputPath = path.join(dir, `${fileName}_${width}x${height || width}${ext}`);
|
||||
|
||||
await sharp(filePath)
|
||||
.resize(width, height, {
|
||||
fit: 'cover',
|
||||
position: 'center'
|
||||
})
|
||||
.jpeg({ quality: 85 })
|
||||
.toFile(outputPath);
|
||||
|
||||
return outputPath;
|
||||
};
|
||||
|
||||
export const createThumbnail = async (filePath: string): Promise<string> => {
|
||||
return resizeImage(filePath, 300, 300);
|
||||
};
|
||||
|
||||
export const deleteFile = (filePath: string): void => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
};
|
||||
|
||||
export const optimizeImage = async (filePath: string): Promise<void> => {
|
||||
await sharp(filePath)
|
||||
.jpeg({ quality: 85, progressive: true })
|
||||
.png({ compressionLevel: 9 })
|
||||
.webp({ quality: 85 })
|
||||
.toFile(filePath + '.tmp');
|
||||
|
||||
fs.renameSync(filePath + '.tmp', filePath);
|
||||
};
|
||||
|
||||
14
backend/src/utils/jwt.utils.ts
Normal file
14
backend/src/utils/jwt.utils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export const generateToken = (userId: string, role: string): string => {
|
||||
return jwt.sign(
|
||||
{ userId, role },
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
||||
);
|
||||
};
|
||||
|
||||
export const verifyToken = (token: string): { userId: string; role: string } => {
|
||||
return jwt.verify(token, process.env.JWT_SECRET!) as { userId: string; role: string };
|
||||
};
|
||||
|
||||
14
backend/src/utils/password.utils.ts
Normal file
14
backend/src/utils/password.utils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export const hashPassword = async (password: string): Promise<string> => {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
return bcrypt.hash(password, salt);
|
||||
};
|
||||
|
||||
export const comparePassword = async (
|
||||
password: string,
|
||||
hashedPassword: string
|
||||
): Promise<boolean> => {
|
||||
return bcrypt.compare(password, hashedPassword);
|
||||
};
|
||||
|
||||
27
backend/tsconfig.json
Normal file
27
backend/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user