Files
esp32DIY_web/backend/prisma/schema.prisma
root 6d11a9c1cc feat: board info logging, anomaly detection, re-flash & firmware update tokens
- Flash.jsx: replace esp-web-tools with direct esptool-js integration
  → reads MAC address + chip type before flash via Web Serial API
  → step-by-step UI (connect → board info → download → flash → done)
  → retry button on failure with remaining-attempt counter
  → firmware update token request after successful flash

- Schema: FlashToken (maxAttempts/attemptCount/isLocked/isUpdateToken),
  FlashLog (startedAt/completedAt/durationMs/chipId/flashSize),
  FlashAnomaly model (RATE_LIMIT_IP/HIGH_VOLUME_IP/MAC_REUSE/TOKEN_LOCK/SUSPICIOUS_DURATION),
  ProjectFile.firmwareVersion

- flash.js: new POST /start (board info + IP log + anomaly detection),
  updated POST /consume (timing, lock on exhaustion), GET returns firmwareFiles

- orders.js: POST /request-reflash (free firmware update token for paid orders),
  updated to flashTokens[] relation

- admin.js: GET /flash/metrics, GET/PUT /flash/anomalies, POST /flash/tokens/:id/unlock

- Admin/FlashMetrics.jsx: dashboard with today stats, recent logs table,
  top-IP chart, anomaly management with resolve button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 05:48:29 +09:00

247 lines
6.7 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
admin
seller
buyer
}
enum ProjectStatus {
draft
pending
approved
rejected
suspended
}
enum OrderStatus {
pending
paid
cancelled
refunded
}
enum PaymentGateway {
toss
stripe
}
model User {
id String @id @default(uuid())
email String @unique
passwordHash String
nickname String @unique
role UserRole @default(buyer)
profileImageUrl String?
isEmailVerified Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
lastLoginIp String?
projects Project[]
orders Order[]
reviews Review[]
auditLogs AuditLog[]
}
model Project {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
title String
description String @db.Text
difficultyLevel Int @default(3)
chipFamily String @default("ESP32-S3")
requiredParts Json?
status ProjectStatus @default(draft)
adminNote String?
commissionRate Float @default(0.1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
files ProjectFile[]
product Product?
}
model ProjectFile {
id String @id @default(uuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
fileType String
url String
thumbnailUrl String?
fileSize Int
mimeType String
originalName String?
flashOffset String @default("0x0")
firmwareVersion String? // e.g. "1.0.0" — firmware 파일에만 사용
displayOrder Int @default(0)
createdAt DateTime @default(now())
}
model Product {
id String @id @default(uuid())
projectId String @unique
project Project @relation(fields: [projectId], references: [id])
price Int
isOnSale Boolean @default(true)
totalSales Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
reviews Review[]
}
model Order {
id String @id @default(uuid())
buyerId String
buyer User @relation(fields: [buyerId], references: [id])
productId String
product Product @relation(fields: [productId], references: [id])
amount Int
commissionAmount Int
sellerAmount Int
paymentGateway PaymentGateway @default(toss)
paymentKey String?
tossOrderId String? @unique
status OrderStatus @default(pending)
buyerInfo Json
deviceInfo Json?
orderedAt DateTime @default(now())
paidAt DateTime?
refundedAt DateTime?
refundReason String?
flashTokens FlashToken[] // 원본 + 업데이트 토큰 복수 허용
review Review?
}
model FlashToken {
id String @id @default(uuid())
token String @unique @default(uuid())
orderId String
order Order @relation(fields: [orderId], references: [id])
isUsed Boolean @default(false) // 성공적으로 플래시된 경우 true
usedAt DateTime?
macAddress String?
chipFamily String?
expiresAt DateTime
createdAt DateTime @default(now())
maxAttempts Int @default(3) // 최대 시도 횟수
attemptCount Int @default(0) // 현재 시도 횟수 (실패 포함)
lastAttemptAt DateTime?
isLocked Boolean @default(false) // maxAttempts 초과 시 잠금
lockedReason String?
isUpdateToken Boolean @default(false) // 펌웨어 업데이트용 재발급 토큰
flashLogs FlashLog[]
@@index([orderId])
}
model FlashLog {
id String @id @default(uuid())
flashTokenId String
flashToken FlashToken @relation(fields: [flashTokenId], references: [id])
attemptNumber Int @default(1)
macAddress String? // 플래시 전 보드에서 읽은 MAC 주소
chipFamily String? // 칩 종류 (esptool-js 에서 읽음)
chipId String? // 칩 ID
flashSize String? // 플래시 메모리 크기
firmwareName String
firmwareId String
success Boolean
errorMessage String?
clientIp String
userAgent String?
startedAt DateTime? // 플래시 시작 시각
completedAt DateTime? // 플래시 완료/실패 시각
durationMs Int? // 소요 시간 ms
flashedAt DateTime @default(now())
@@index([flashTokenId])
@@index([clientIp])
@@index([macAddress])
@@index([flashedAt])
}
// 이상 감지 이벤트
model FlashAnomaly {
id String @id @default(uuid())
type String // RATE_LIMIT_IP | HIGH_VOLUME_IP | MAC_REUSE | TOKEN_LOCK | SUSPICIOUS_DURATION
severity String @default("medium") // low | medium | high
description String
clientIp String?
macAddress String?
flashTokenId String?
flashLogId String?
metadata Json?
resolved Boolean @default(false)
resolvedAt DateTime?
resolvedBy String?
createdAt DateTime @default(now())
@@index([type])
@@index([clientIp])
@@index([resolved])
@@index([createdAt])
}
model Review {
id String @id @default(uuid())
orderId String @unique
order Order @relation(fields: [orderId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
productId String
product Product @relation(fields: [productId], references: [id])
rating Int
title String
content String @db.Text
isVisible Boolean @default(true)
createdAt DateTime @default(now())
media ReviewMedia[]
}
model ReviewMedia {
id String @id @default(uuid())
reviewId String
review Review @relation(fields: [reviewId], references: [id], onDelete: Cascade)
mediaType String
url String
thumbnailUrl String?
createdAt DateTime @default(now())
}
model AuditLog {
id String @id @default(uuid())
userId String?
user User? @relation(fields: [userId], references: [id])
action String
targetType String?
targetId String?
ipAddress String
userAgent String?
requestMethod String?
requestPath String?
requestBody Json?
responseStatus Int?
metadata Json?
createdAt DateTime @default(now())
@@index([userId])
@@index([action])
@@index([createdAt])
}