diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6054ce9..631c5c3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -57,6 +57,14 @@ model User { whatsapp String? birthDate String? @map("birth_date") avatarUrl String? @map("avatar_url") @db.Text + cep String? + address String? + number String? + complement String? + reference String? + neighborhood String? + city String? + state String? trialEndsAt DateTime? @map("trial_ends_at") resetPasswordToken String? @map("reset_password_token") @db.Text resetPasswordExpires DateTime? @map("reset_password_expires") @@ -81,6 +89,7 @@ model Course { certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode") isLocked Boolean @default(false) @map("is_locked") price Float? + order Int @default(0) createdAt DateTime @default(now()) @map("created_at") modules Module[] diff --git a/server.ts b/server.ts index 58fe39b..8750db7 100644 --- a/server.ts +++ b/server.ts @@ -394,7 +394,7 @@ app.get('/api/auth/me', authenticateToken, async (req: AuthenticatedRequest, res // 3.1. Profile: Update Data app.put('/api/users/profile', authenticateToken, async (req: AuthenticatedRequest, res: Response) => { - const { name, whatsapp, birthDate, cpf } = req.body; + const { name, whatsapp, birthDate, cpf, cep, address, number, complement, reference, neighborhood, city, state } = req.body; const userId = req.user!.id; try { @@ -404,7 +404,15 @@ app.put('/api/users/profile', authenticateToken, async (req: AuthenticatedReques ...(name !== undefined && { name }), ...(whatsapp !== undefined && { whatsapp }), ...(birthDate !== undefined && { birthDate }), - ...(cpf !== undefined && { cpf }) + ...(cpf !== undefined && { cpf }), + ...(cep !== undefined && { cep }), + ...(address !== undefined && { address }), + ...(number !== undefined && { number }), + ...(complement !== undefined && { complement }), + ...(reference !== undefined && { reference }), + ...(neighborhood !== undefined && { neighborhood }), + ...(city !== undefined && { city }), + ...(state !== undefined && { state }) } }); res.json({ success: true, user }); @@ -413,6 +421,42 @@ app.put('/api/users/profile', authenticateToken, async (req: AuthenticatedReques } }); +// 3.1.1 Profile: Change Password +app.post('/api/users/change-password', authenticateToken, async (req: AuthenticatedRequest, res: Response) => { + const { currentPassword, newPassword } = req.body; + const userId = req.user!.id; + + if (!currentPassword || !newPassword) { + res.status(400).json({ error: 'Senha atual e nova senha são obrigatórias.' }); + return; + } + + try { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + res.status(404).json({ error: 'Usuário não encontrado' }); + return; + } + + const isMatch = bcrypt.compareSync(currentPassword, user.password); + if (!isMatch) { + res.status(400).json({ error: 'Senha atual incorreta.' }); + return; + } + + const hashedPassword = bcrypt.hashSync(newPassword, 10); + await prisma.user.update({ + where: { id: userId }, + data: { password: hashedPassword } + }); + + res.json({ success: true, message: 'Senha alterada com sucesso.' }); + } catch (err: any) { + console.error('Erro ao alterar senha:', err); + res.status(500).json({ error: 'Erro interno ao alterar senha' }); + } +}); + // 3.2. Profile: Avatar Upload app.post('/api/users/avatar', authenticateToken, upload.single('avatar'), async (req: AuthenticatedRequest, res: Response) => { const userId = req.user!.id; @@ -484,6 +528,7 @@ app.get('/api/courses', authenticateToken, async (req: AuthenticatedRequest, res const isAdmin = user?.role === 'admin'; const courses = await prisma.course.findMany({ + orderBy: { order: 'asc' }, include: { modules: { include: { @@ -1518,6 +1563,8 @@ app.post('/api/admin/courses', authenticateToken, requireAdmin, async (req: Auth return; } + const count = await prisma.course.count(); + const newCourse = await prisma.course.create({ data: { title, @@ -1525,7 +1572,8 @@ app.post('/api/admin/courses', authenticateToken, requireAdmin, async (req: Auth thumbnail: thumbnail, category, isLocked: isLocked === true || isLocked === 'true', - price: price ? Number(price) : 0 + price: price ? Number(price) : 0, + order: count } }); @@ -1677,7 +1725,7 @@ app.delete('/api/admin/lessons/:id', authenticateToken, requireAdmin, async (req } }); -// Reorder modules or lessons (Drag-and-drop or batch reorder helper) +// Reorder modules, lessons or courses (Drag-and-drop or batch reorder helper) app.post('/api/admin/reorder', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { const { type, items } = req.body; @@ -1701,6 +1749,13 @@ app.post('/api/admin/reorder', authenticateToken, requireAdmin, async (req: Auth data: { order: Number(item.order) } }); } + } else if (type === 'courses') { + for (const item of items) { + await prisma.course.update({ + where: { id: item.id }, + data: { order: Number(item.order) } + }); + } } res.json({ success: true }); } catch (error) { diff --git a/src/components/AdminContentManager.tsx b/src/components/AdminContentManager.tsx index 503fea0..314008a 100644 --- a/src/components/AdminContentManager.tsx +++ b/src/components/AdminContentManager.tsx @@ -33,6 +33,7 @@ interface Course { isLocked?: boolean; price?: number; certificateMode?: string; + order?: number; } interface AdminContentManagerProps { @@ -52,7 +53,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) const [showCourseForm, setShowCourseForm] = useState(false); const [editingCourse, setEditingCourse] = useState(null); const [courseForm, setCourseForm] = useState({ - title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' as 'FULL_COURSE' | 'PER_MODULE' + title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' as 'FULL_COURSE' | 'PER_MODULE', order: 0 }); const [showModuleForm, setShowModuleForm] = useState(false); @@ -199,7 +200,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) await fetchCourses(); setShowCourseForm(false); setEditingCourse(null); - setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE' }); + setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0, certificateMode: 'FULL_COURSE', order: 0 }); showSuccess(editingCourse ? 'Curso atualizado com sucesso!' : 'Curso criado com sucesso!'); } else { const errData = await res.json(); @@ -220,7 +221,8 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) category: course.category, isLocked: course.isLocked || false, price: course.price || 0, - certificateMode: (course.certificateMode || 'FULL_COURSE') as 'PER_MODULE' | 'FULL_COURSE' + certificateMode: (course.certificateMode || 'FULL_COURSE') as 'PER_MODULE' | 'FULL_COURSE', + order: course.order || 0 }); setShowCourseForm(true); setTimeout(() => { @@ -459,7 +461,7 @@ export default function AdminContentManager({ token }: AdminContentManagerProps) {!selectedCourse && !showCourseForm && ( {!isEditing ? ( - +
+ + +
) : (
+ {/* Address Area */} +
+

+ Meu endereço +

+ +
+ {/* CEP */} +
+

CEP

+ {isEditing ? ( + setCep(e.target.value.replace(/\D/g, '').replace(/(\d{5})(\d)/, '$1-$2'))} + maxLength={9} + placeholder="00000-000" + className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + /> + ) : ( +

{cep || 'Não informado'}

+ )} +
+
+ + {/* Endereço */} +
+

Endereço

+ {isEditing ? ( + setAddress(e.target.value)} + className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + /> + ) : ( +

{address || 'Não informado'}

+ )} +
+ + {/* Número */} +
+

Número

+ {isEditing ? ( + setNumber(e.target.value)} + className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + /> + ) : ( +

{number || 'Não informado'}

+ )} +
+ + {/* Complemento */} +
+

Complemento

+ {isEditing ? ( + setComplement(e.target.value)} + className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + /> + ) : ( +

{complement || 'Não informado'}

+ )} +
+ + {/* Referência */} +
+

Referência

+ {isEditing ? ( + setReference(e.target.value)} + className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + /> + ) : ( +

{reference || 'Não informado'}

+ )} +
+ + {/* Bairro */} +
+

Bairro

+ {isEditing ? ( + setNeighborhood(e.target.value)} + className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + /> + ) : ( +

{neighborhood || 'Não informado'}

+ )} +
+ + {/* Cidade */} +
+

Cidade

+ {isEditing ? ( + setCity(e.target.value)} + className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + /> + ) : ( +

{city || 'Não informado'}

+ )} +
+ + {/* Estado */} +
+

Estado

+ {isEditing ? ( + setState(e.target.value.toUpperCase())} + maxLength={2} + className="w-full bg-black/50 border border-white/10 rounded-md p-1.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + /> + ) : ( +

{state || 'Não informado'}

+ )} +
+
+
+ {user.role === 'admin' && (
@@ -299,6 +506,77 @@ export default function ProfileView({ user, onBack, onUpdateUser }: ProfileViewP
+ + {showPasswordModal && ( +
+
+ + +
+ +

Alterar Senha

+
+ +
+
+ + setCurrentPassword(e.target.value)} + className="w-full glass-input rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + required + /> +
+ +
+ + setNewPassword(e.target.value)} + className="w-full glass-input rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + required + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="w-full glass-input rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-[#E50914]" + required + /> +
+ +
+ + +
+
+
+
+ )} ); } diff --git a/src/types.ts b/src/types.ts index 5a03039..e4c91d5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,14 @@ export interface User { whatsapp?: string; avatarUrl?: string; trialEndsAt?: string | null; + cep?: string; + address?: string; + number?: string; + complement?: string; + reference?: string; + neighborhood?: string; + city?: string; + state?: string; createdAt: string; }