Compare commits
6 Commits
444c867ae8
...
df1d63019e
| Author | SHA1 | Date |
|---|---|---|
|
|
df1d63019e | |
|
|
6a16d97161 | |
|
|
cc9b906eeb | |
|
|
8236ade11a | |
|
|
22ecf41e9d | |
|
|
320b627a91 |
|
|
@ -21,6 +21,7 @@ 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:
|
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`
|
* **Container**: `postgres-microtecflix`
|
||||||
* **Imagem**: `postgres:16-alpine`
|
* **Imagem**: `postgres:16-alpine`
|
||||||
* **Porta Host**: `5435:5432` (evita colisão com a porta 5432 original)
|
* **Porta Host**: `5435:5432` (evita colisão com a porta 5432 original)
|
||||||
|
|
|
||||||
11
memory.md
11
memory.md
|
|
@ -27,14 +27,21 @@ O **MicrotecFlix** é uma plataforma EAD / Streaming de cursos de informática e
|
||||||
|
|
||||||
## 2. Esquema de Entidades (`prisma/schema.prisma` / PostgreSQL)
|
## 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`).
|
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.
|
2. **`Course`**: Cursos cadastrados com título, descrição, thumbnail, categoria e preço avulso/bloqueio.
|
||||||
3. **`Module`**: Módulos organizados sequencialmente por curso.
|
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.
|
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.
|
5. **`Note`**: Anotações pessoais feitas pelos alunos por aula.
|
||||||
6. **`Progress`**: Histórico de progresso do aluno por aula (% assistida e `completed`).
|
6. **`Progress`**: Histórico de progresso do aluno por aula (% assistida e `completed`).
|
||||||
7. **`Payment`**: Histórico de faturas e cobranças registradas no gateway de pagamentos.
|
7. **`Payment` (SubscriptionPayment)**: Histórico de faturas e cobranças registradas no gateway de pagamentos, linkado a usuários, cursos e planos.
|
||||||
8. **`WebhookLog`**: Registro auditável de webhooks recebidos do Asaas.
|
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,6 +13,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.700.0",
|
"@aws-sdk/client-s3": "^3.700.0",
|
||||||
"@google/genai": "^2.4.0",
|
"@google/genai": "^2.4.0",
|
||||||
|
"@prisma/client": "^6.4.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
"prisma": "^6.4.1",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,11 @@ model User {
|
||||||
role Role @default(student)
|
role Role @default(student)
|
||||||
subscriptionStatus SubscriptionStatus @default(TRIAL) @map("subscription_status")
|
subscriptionStatus SubscriptionStatus @default(TRIAL) @map("subscription_status")
|
||||||
asaasCustomerId String? @map("asaas_customer_id")
|
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")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
notes Note[]
|
notes Note[]
|
||||||
|
|
@ -60,6 +65,7 @@ model User {
|
||||||
payments Payment[]
|
payments Payment[]
|
||||||
moduleGrades ModuleGrade[]
|
moduleGrades ModuleGrade[]
|
||||||
certificates Certificate[]
|
certificates Certificate[]
|
||||||
|
unlockedCourses String[] @default([]) @map("unlocked_courses")
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +77,8 @@ model Course {
|
||||||
thumbnail String
|
thumbnail String
|
||||||
category String
|
category String
|
||||||
certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
|
certificateMode CertificateMode @default(FULL_COURSE) @map("certificate_mode")
|
||||||
|
isLocked Boolean @default(false) @map("is_locked")
|
||||||
|
price Float?
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
modules Module[]
|
modules Module[]
|
||||||
|
|
@ -147,6 +155,9 @@ model Payment {
|
||||||
invoiceUrl String @map("invoice_url") @db.Text
|
invoiceUrl String @map("invoice_url") @db.Text
|
||||||
dueDate DateTime @map("due_date") @db.Date
|
dueDate DateTime @map("due_date") @db.Date
|
||||||
paidAt DateTime? @map("paid_at")
|
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")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
@ -205,3 +216,29 @@ model Certificate {
|
||||||
|
|
||||||
@@map("certificates")
|
@@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")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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 ? '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 ? '💰 Pago (Bloqueado)' : '🔓 Gratuito (Livre)'}
|
{localLocked ? <span>💰 Pago (Bloqueado)</span> : <span>🔓 Gratuito (Livre)</span>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ function CoursePricingCard({ course, saveLoading, onSave }: { course: Course, sa
|
||||||
) : (
|
) : (
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
Salvar Alteração
|
<span>Salvar Alteração</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -82,7 +82,7 @@ export const AdminPricingManager: React.FC<AdminPricingManagerProps> = ({ course
|
||||||
const [showPlanModal, setShowPlanModal] = useState(false);
|
const [showPlanModal, setShowPlanModal] = useState(false);
|
||||||
const [planForm, setPlanForm] = useState<Partial<Plan>>({ cycle: 'LIFETIME', active: true, includedCourses: [] });
|
const [planForm, setPlanForm] = useState<Partial<Plan>>({ cycle: 'LIFETIME', active: true, includedCourses: [] });
|
||||||
|
|
||||||
const token = localStorage.getItem('devflix_token');
|
const token = localStorage.getItem('devflix_admin_token');
|
||||||
|
|
||||||
const fetchPlans = async () => {
|
const fetchPlans = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,13 @@ export default function Navbar({ user, activeView, setView, onLogout }: NavbarPr
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
<span>Meus dados</span>
|
<span>Meus dados</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => { setView('help'); setShowProfileMenu(false); }}
|
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"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export const prisma = new PrismaClient();
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
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