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:
root
2025-11-22 01:00:35 +00:00
commit 5e4a2283bf
51 changed files with 5158 additions and 0 deletions

51
backend/package.json Normal file
View 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"
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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
View 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"]
}