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>
This commit is contained in:
root
2026-05-22 05:48:29 +09:00
parent 182782f271
commit 6d11a9c1cc
9 changed files with 1206 additions and 293 deletions

View File

@@ -73,18 +73,19 @@ model Project {
}
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")
displayOrder Int @default(0)
createdAt DateTime @default(now())
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 {
@@ -121,38 +122,79 @@ model Order {
refundedAt DateTime?
refundReason String?
flashToken FlashToken?
review Review?
flashTokens FlashToken[] // 원본 + 업데이트 토큰 복수 허용
review Review?
}
model FlashToken {
id String @id @default(uuid())
token String @unique @default(uuid())
orderId String @unique
order Order @relation(fields: [orderId], references: [id])
isUsed Boolean @default(false)
usedAt DateTime?
macAddress String?
chipFamily String?
expiresAt DateTime
createdAt DateTime @default(now())
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) // 펌웨어 업데이트용 재발급 토큰
flashLog FlashLog?
flashLogs FlashLog[]
@@index([orderId])
}
model FlashLog {
id String @id @default(uuid())
flashTokenId String @unique
flashToken FlashToken @relation(fields: [flashTokenId], references: [id])
macAddress String
chipFamily String
firmwareName String
firmwareId String
success Boolean
errorMessage String?
clientIp String
userAgent String?
flashedAt DateTime @default(now())
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 {