Compare commits
No commits in common. "df1d63019eb79ebab0644440c62750c28e6a75d9" and "444c867ae8b3893ffc3d97e6a45c40ad18beb316" have entirely different histories.
df1d63019e
...
444c867ae8
|
|
@ -21,7 +21,6 @@ O sistema realiza o roteamento automático do frontend via `src/Root.tsx`:
|
|||
|
||||
Para evitar conflitos com outros bancos de dados ativos na VPS (como o PostgreSQL do AgendaPRO na porta `5432`), o MicrotecFlix utiliza uma instância isolada e dedicada de PostgreSQL:
|
||||
|
||||
* **Tecnologia ORM**: Prisma ORM (`prisma/schema.prisma`), que governa 100% da persistência de dados. O backend (`server-prisma.ts`) está inteiramente refatorado, sem utilizar mais arquivos `db.json` e nem funções locais (como `loadDb()`). Todas as alterações agora salvam em tabelas.
|
||||
* **Container**: `postgres-microtecflix`
|
||||
* **Imagem**: `postgres:16-alpine`
|
||||
* **Porta Host**: `5435:5432` (evita colisão com a porta 5432 original)
|
||||
|
|
|
|||
11
memory.md
11
memory.md
|
|
@ -27,21 +27,14 @@ O **MicrotecFlix** é uma plataforma EAD / Streaming de cursos de informática e
|
|||
|
||||
## 2. Esquema de Entidades (`prisma/schema.prisma` / PostgreSQL)
|
||||
|
||||
Todo o sistema foi **100% migrado para o PostgreSQL** utilizando o **Prisma ORM**. O antigo arquivo `db.json` e a função genérica `loadDb()` foram completamente descartados e removidos da base de código backend (`server-prisma.ts`).
|
||||
|
||||
1. **`User`**: Usuários da plataforma (`admin` ou `student`) com status de assinatura (`ACTIVE`, `OVERDUE`, `CANCELED`, `TRIAL`).
|
||||
2. **`Course`**: Cursos cadastrados com título, descrição, thumbnail, categoria e preço avulso/bloqueio.
|
||||
2. **`Course`**: Cursos cadastrados com título, descrição, thumbnail, categoria e preço.
|
||||
3. **`Module`**: Módulos organizados sequencialmente por curso.
|
||||
4. **`Lesson`**: Aulas vinculadas ao módulo com URL de vídeo (`youtube` ou `direct`) e conteúdo complementar.
|
||||
5. **`Note`**: Anotações pessoais feitas pelos alunos por aula.
|
||||
6. **`Progress`**: Histórico de progresso do aluno por aula (% assistida e `completed`).
|
||||
7. **`Payment` (SubscriptionPayment)**: Histórico de faturas e cobranças registradas no gateway de pagamentos, linkado a usuários, cursos e planos.
|
||||
7. **`Payment`**: Histórico de faturas e cobranças registradas no gateway de pagamentos.
|
||||
8. **`WebhookLog`**: Registro auditável de webhooks recebidos do Asaas.
|
||||
9. **`ModuleActivity` / `ModuleGrade`**: Questionários/Múltipla escolha e pontuações dos alunos, substituindo a antiga matriz JSON de atividades.
|
||||
10. **`Certificate`**: Certificados ganhos pelos alunos (por Módulo ou por Curso) gerados automaticamente.
|
||||
11. **`Plan`**: Planos de assinatura (Combos vitálcios, Assinaturas Mensais, Anuais).
|
||||
12. **`Notification`**: Alertas e avisos aos usuários, gerenciados em tabela relacional em tempo real.
|
||||
13. **`SystemSettings`**: Configurações dinâmicas do sistema, chaves do Asaas e texto de ajuda.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.700.0",
|
||||
"@google/genai": "^2.4.0",
|
||||
"@prisma/client": "^6.4.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
|
|
@ -36,7 +35,6 @@
|
|||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"esbuild": "^0.25.0",
|
||||
"prisma": "^6.4.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
|
|
|
|||
|
|
@ -53,11 +53,6 @@ model User {
|
|||
role Role @default(student)
|
||||
subscriptionStatus SubscriptionStatus @default(TRIAL) @map("subscription_status")
|
||||
asaasCustomerId String? @map("asaas_customer_id")
|
||||
cpf String?
|
||||
whatsapp String?
|
||||
birthDate String? @map("birth_date")
|
||||
avatarUrl String? @map("avatar_url") @db.Text
|
||||
trialEndsAt DateTime? @map("trial_ends_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
notes Note[]
|
||||
|
|
@ -65,7 +60,6 @@ model User {
|
|||
payments Payment[]
|
||||
moduleGrades ModuleGrade[]
|
||||
certificates Certificate[]
|
||||
unlockedCourses String[] @default([]) @map("unlocked_courses")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
|
@ -77,8 +71,6 @@ model Course {
|
|||
thumbnail String
|
||||
category String
|
||||
certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
|
||||
isLocked Boolean @default(false) @map("is_locked")
|
||||
price Float?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
modules Module[]
|
||||
|
|
@ -155,9 +147,6 @@ model Payment {
|
|||
invoiceUrl String @map("invoice_url") @db.Text
|
||||
dueDate DateTime @map("due_date") @db.Date
|
||||
paidAt DateTime? @map("paid_at")
|
||||
planId String? @map("plan_id")
|
||||
courseId String? @map("course_id")
|
||||
description String? @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
|
@ -216,29 +205,3 @@ model Certificate {
|
|||
|
||||
@@map("certificates")
|
||||
}
|
||||
|
||||
model Plan {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String @db.Text
|
||||
price Float
|
||||
cycle String // 'MONTHLY', 'YEARLY', 'LIFETIME'
|
||||
active Boolean @default(true)
|
||||
includedCourses String[] @default([]) @map("included_courses")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@map("plans")
|
||||
}
|
||||
|
||||
model SystemSettings {
|
||||
id String @id @default("default")
|
||||
trialDurationDays Int @default(7) @map("trial_duration_days")
|
||||
asaasEnvironment String @default("sandbox") @map("asaas_environment")
|
||||
asaasApiKey String @default("") @map("asaas_api_key")
|
||||
asaasWebhookUrl String @default("") @map("asaas_webhook_url")
|
||||
asaasWebhookSecret String @default("") @map("asaas_webhook_secret")
|
||||
helpText String @db.Text @default("# Central de Ajuda") @map("help_text")
|
||||
|
||||
@@map("system_settings")
|
||||
}
|
||||
|
||||
|
|
|
|||
1760
server-json.ts
1760
server-json.ts
File diff suppressed because it is too large
Load Diff
|
|
@ -32,7 +32,7 @@ function CoursePricingCard({ course, saveLoading, onSave }: { course: Course, sa
|
|||
localLocked ? 'bg-amber-500/20 text-amber-400 border border-amber-500/30' : 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
|
||||
}`}
|
||||
>
|
||||
{localLocked ? <span>💰 Pago (Bloqueado)</span> : <span>🔓 Gratuito (Livre)</span>}
|
||||
{localLocked ? '💰 Pago (Bloqueado)' : '🔓 Gratuito (Livre)'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ function CoursePricingCard({ course, saveLoading, onSave }: { course: Course, sa
|
|||
) : (
|
||||
<Save className="w-4 h-4" />
|
||||
)}
|
||||
<span>Salvar Alteração</span>
|
||||
Salvar Alteração
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -82,7 +82,7 @@ export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ course
|
|||
const [showPlanModal, setShowPlanModal] = useState(false);
|
||||
const [planForm, setPlanForm] = useState<Partial<Plan>>({ cycle: 'LIFETIME', active: true, includedCourses: [] });
|
||||
|
||||
const token = localStorage.getItem('devflix_admin_token');
|
||||
const token = localStorage.getItem('devflix_token');
|
||||
|
||||
const fetchPlans = async () => {
|
||||
setLoading(true);
|
||||
|
|
|
|||
|
|
@ -220,13 +220,6 @@ export default function Navbar({ user, activeView, setView, onLogout }: NavbarPr
|
|||
<User className="w-4 h-4" />
|
||||
<span>Meus dados</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setView('certificates'); setShowProfileMenu(false); }}
|
||||
className="w-full px-4 py-2 text-left flex items-center space-x-2 text-sm text-zinc-300 hover:bg-zinc-800 hover:text-white transition-colors"
|
||||
>
|
||||
<GraduationCap className="w-4 h-4" />
|
||||
<span>Meu Certificado</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setView('help'); setShowProfileMenu(false); }}
|
||||
className="w-full px-4 py-2 text-left flex items-center space-x-2 text-sm text-zinc-300 hover:bg-zinc-800 hover:text-white transition-colors"
|
||||
|
|
|
|||
|
|
@ -1,242 +0,0 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const DATA_DIR = process.env.DATA_DIR || (fs.existsSync('/app/data') ? '/app/data' : process.cwd());
|
||||
const DB_FILE = path.join(DATA_DIR, 'database.json');
|
||||
|
||||
async function main() {
|
||||
console.log('🔄 Iniciando migração de JSON para PostgreSQL...');
|
||||
|
||||
if (!fs.existsSync(DB_FILE)) {
|
||||
console.error('❌ Arquivo database.json não encontrado!');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(DB_FILE, 'utf-8'));
|
||||
|
||||
// 1. Settings
|
||||
console.log('Migrando Settings...');
|
||||
if (data.settings) {
|
||||
await prisma.systemSettings.upsert({
|
||||
where: { id: 'default' },
|
||||
update: {
|
||||
trialDurationDays: data.settings.trialDurationDays || 7,
|
||||
asaasEnvironment: data.settings.asaasEnvironment || 'sandbox',
|
||||
asaasApiKey: data.settings.asaasApiKey || '',
|
||||
asaasWebhookUrl: data.settings.asaasWebhookUrl || '',
|
||||
asaasWebhookSecret: data.settings.asaasWebhookSecret || '',
|
||||
helpText: data.settings.helpText || '# Central de Ajuda'
|
||||
},
|
||||
create: {
|
||||
id: 'default',
|
||||
trialDurationDays: data.settings.trialDurationDays || 7,
|
||||
asaasEnvironment: data.settings.asaasEnvironment || 'sandbox',
|
||||
asaasApiKey: data.settings.asaasApiKey || '',
|
||||
asaasWebhookUrl: data.settings.asaasWebhookUrl || '',
|
||||
asaasWebhookSecret: data.settings.asaasWebhookSecret || '',
|
||||
helpText: data.settings.helpText || '# Central de Ajuda'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Users
|
||||
console.log(`Migrando ${data.users?.length || 0} Users...`);
|
||||
for (const user of data.users || []) {
|
||||
await prisma.user.upsert({
|
||||
where: { id: user.id },
|
||||
update: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
role: user.role === 'admin' ? 'admin' : 'student',
|
||||
subscriptionStatus: user.subscriptionStatus,
|
||||
asaasCustomerId: user.asaasCustomerId,
|
||||
cpf: user.cpf,
|
||||
whatsapp: user.whatsapp,
|
||||
birthDate: user.birthDate,
|
||||
avatarUrl: user.avatarUrl,
|
||||
trialEndsAt: user.trialEndsAt ? new Date(user.trialEndsAt) : null,
|
||||
unlockedCourses: user.unlockedCourses || [],
|
||||
createdAt: new Date(user.createdAt)
|
||||
},
|
||||
create: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
role: user.role === 'admin' ? 'admin' : 'student',
|
||||
subscriptionStatus: user.subscriptionStatus,
|
||||
asaasCustomerId: user.asaasCustomerId,
|
||||
cpf: user.cpf,
|
||||
whatsapp: user.whatsapp,
|
||||
birthDate: user.birthDate,
|
||||
avatarUrl: user.avatarUrl,
|
||||
trialEndsAt: user.trialEndsAt ? new Date(user.trialEndsAt) : null,
|
||||
unlockedCourses: user.unlockedCourses || [],
|
||||
createdAt: new Date(user.createdAt)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Courses
|
||||
console.log(`Migrando ${data.courses?.length || 0} Courses...`);
|
||||
for (const course of data.courses || []) {
|
||||
await prisma.course.upsert({
|
||||
where: { id: course.id },
|
||||
update: {
|
||||
title: course.title,
|
||||
description: course.description,
|
||||
thumbnail: course.thumbnail,
|
||||
category: course.category,
|
||||
certificateMode: course.certificateMode || 'FULL_COURSE',
|
||||
isLocked: course.isLocked || false,
|
||||
price: course.price || null,
|
||||
createdAt: new Date(course.createdAt)
|
||||
},
|
||||
create: {
|
||||
id: course.id,
|
||||
title: course.title,
|
||||
description: course.description,
|
||||
thumbnail: course.thumbnail,
|
||||
category: course.category,
|
||||
certificateMode: course.certificateMode || 'FULL_COURSE',
|
||||
isLocked: course.isLocked || false,
|
||||
price: course.price || null,
|
||||
createdAt: new Date(course.createdAt)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Modules
|
||||
console.log(`Migrando ${data.modules?.length || 0} Modules...`);
|
||||
for (const mod of data.modules || []) {
|
||||
await prisma.module.upsert({
|
||||
where: { id: mod.id },
|
||||
update: {
|
||||
courseId: mod.courseId,
|
||||
title: mod.title,
|
||||
order: mod.order,
|
||||
createdAt: new Date(mod.createdAt)
|
||||
},
|
||||
create: {
|
||||
id: mod.id,
|
||||
courseId: mod.courseId,
|
||||
title: mod.title,
|
||||
order: mod.order,
|
||||
createdAt: new Date(mod.createdAt)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Lessons
|
||||
console.log(`Migrando ${data.lessons?.length || 0} Lessons...`);
|
||||
for (const lesson of data.lessons || []) {
|
||||
await prisma.lesson.upsert({
|
||||
where: { id: lesson.id },
|
||||
update: {
|
||||
moduleId: lesson.moduleId,
|
||||
title: lesson.title,
|
||||
videoUrl: lesson.videoUrl,
|
||||
videoType: lesson.videoType === 'youtube' ? 'youtube' : 'direct',
|
||||
order: lesson.order,
|
||||
content: lesson.content,
|
||||
createdAt: new Date(lesson.createdAt)
|
||||
},
|
||||
create: {
|
||||
id: lesson.id,
|
||||
moduleId: lesson.moduleId,
|
||||
title: lesson.title,
|
||||
videoUrl: lesson.videoUrl,
|
||||
videoType: lesson.videoType === 'youtube' ? 'youtube' : 'direct',
|
||||
order: lesson.order,
|
||||
content: lesson.content,
|
||||
createdAt: new Date(lesson.createdAt)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Plans
|
||||
console.log(`Migrando ${data.plans?.length || 0} Plans...`);
|
||||
for (const plan of data.plans || []) {
|
||||
await prisma.plan.upsert({
|
||||
where: { id: plan.id },
|
||||
update: {
|
||||
title: plan.title,
|
||||
description: plan.description || '',
|
||||
price: plan.price,
|
||||
cycle: plan.cycle,
|
||||
active: plan.active,
|
||||
includedCourses: plan.includedCourses || [],
|
||||
createdAt: plan.createdAt ? new Date(plan.createdAt) : new Date()
|
||||
},
|
||||
create: {
|
||||
id: plan.id,
|
||||
title: plan.title,
|
||||
description: plan.description || '',
|
||||
price: plan.price,
|
||||
cycle: plan.cycle,
|
||||
active: plan.active,
|
||||
includedCourses: plan.includedCourses || [],
|
||||
createdAt: plan.createdAt ? new Date(plan.createdAt) : new Date()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 7. Payments
|
||||
console.log(`Migrando ${data.payments?.length || 0} Payments...`);
|
||||
for (const p of data.payments || []) {
|
||||
let billingType = 'PIX';
|
||||
if (p.billingType === 'BOLETO') billingType = 'BOLETO';
|
||||
if (p.billingType === 'CREDIT_CARD') billingType = 'CREDIT_CARD';
|
||||
|
||||
let status = 'PENDING';
|
||||
if (p.status === 'RECEIVED') status = 'RECEIVED';
|
||||
if (p.status === 'CONFIRMED') status = 'CONFIRMED';
|
||||
if (p.status === 'OVERDUE') status = 'OVERDUE';
|
||||
if (p.status === 'REFUNDED') status = 'REFUNDED';
|
||||
|
||||
await prisma.payment.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
userId: p.userId,
|
||||
value: p.value,
|
||||
status: status as any,
|
||||
billingType: billingType as any,
|
||||
invoiceUrl: p.invoiceUrl || '',
|
||||
dueDate: p.dueDate ? new Date(p.dueDate) : new Date(),
|
||||
paidAt: p.paidAt ? new Date(p.paidAt) : null,
|
||||
planId: p.planId || null,
|
||||
courseId: p.courseId || null,
|
||||
description: p.description || null,
|
||||
createdAt: p.createdAt ? new Date(p.createdAt) : new Date()
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
userId: p.userId,
|
||||
value: p.value,
|
||||
status: status as any,
|
||||
billingType: billingType as any,
|
||||
invoiceUrl: p.invoiceUrl || '',
|
||||
dueDate: p.dueDate ? new Date(p.dueDate) : new Date(),
|
||||
paidAt: p.paidAt ? new Date(p.paidAt) : null,
|
||||
planId: p.planId || null,
|
||||
courseId: p.courseId || null,
|
||||
description: p.description || null,
|
||||
createdAt: p.createdAt ? new Date(p.createdAt) : new Date()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ Migração finalizada com sucesso!');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch(e => {
|
||||
console.error('❌ Erro na migração:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
import { Router, Request, Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import multer from 'multer';
|
||||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
|
||||
// Mock types since we will split these out
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: any;
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const router = Router();
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
const s3 = new S3Client({
|
||||
region: 'us-east-1',
|
||||
endpoint: 'https://s3-estudo.microtecinformaticacurso.com.br',
|
||||
credentials: {
|
||||
accessKeyId: 'microtecflix_admin',
|
||||
secretAccessKey: 'MicrotecFlixS3SecurePass2026!'
|
||||
},
|
||||
forcePathStyle: true
|
||||
});
|
||||
|
||||
const authenticateToken = (req: any, res: any, next: any) => {
|
||||
// Logic will be in server.ts or a middleware file, assuming it's passed down
|
||||
next();
|
||||
};
|
||||
|
||||
router.put('/profile', async (req: AuthenticatedRequest, res: Response) => {
|
||||
const { name, whatsapp, birthDate, cpf } = req.body;
|
||||
const userId = req.user!.id;
|
||||
|
||||
try {
|
||||
const user = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
...(name !== undefined && { name }),
|
||||
...(whatsapp !== undefined && { whatsapp }),
|
||||
...(birthDate !== undefined && { birthDate }),
|
||||
...(cpf !== undefined && { cpf })
|
||||
}
|
||||
});
|
||||
res.json({ success: true, user });
|
||||
} catch (error) {
|
||||
res.status(404).json({ error: 'Usuário não encontrado' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/avatar', upload.single('avatar'), async (req: AuthenticatedRequest, res: Response) => {
|
||||
const userId = req.user!.id;
|
||||
if (!req.file) {
|
||||
res.status(400).json({ error: 'Nenhum arquivo enviado' });
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = req.file.originalname.split('.').pop();
|
||||
const filename = `avatars/${userId}_${Date.now()}.${ext}`;
|
||||
|
||||
try {
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: 'microtecflix',
|
||||
Key: filename,
|
||||
Body: req.file.buffer,
|
||||
ContentType: req.file.mimetype
|
||||
}));
|
||||
|
||||
const avatarUrl = `https://s3-estudo.microtecinformaticacurso.com.br/microtecflix/${filename}`;
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { avatarUrl }
|
||||
});
|
||||
|
||||
res.json({ success: true, avatarUrl });
|
||||
} catch (err: any) {
|
||||
console.error('Erro no upload da foto:', err);
|
||||
res.status(500).json({ error: 'Erro ao salvar foto no MinIO' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Loading…
Reference in New Issue