Files
Bonna-Moon-Studio/app/prisma/schema.prisma
tonym 9e21895232 app: OTM integrations — operator-magic + account SSO + suggestion jots + self-update (v0.4.0)
Three new doors, all wire-contract ports of the shared @otm/account-panel
factories (this backbone is plain Node — the Next factories can't mount):

- GET /api/auth/operator-magic — OTM 'Log in as admin'. HS256 verify against
  OPERATOR_SHARED_SECRET (alg allowlist, constant-time, action-claim rejected
  for flow separation), single-use jti via OperatorMagicConsumed (+ who/when
  audit), 1-HOUR session with cookie Max-Age derived from the payload, 🔑
  support-session banner in the app, every failure a 302 reason redirect.
- GET /api/auth/otm-sso — the OTM /account tile. Stricter sso-ticket verify
  (exp mandatory, audience compared), email-then-role actor mapping, safeNext.
- 💡 suggestion jot chip — files a Gitea issue (shared attribution footer,
  8k cap, 10s timeout); on any failure the jot is kept as a note instead.
  Success also leaves a '#N — …' note so she has her own record.
- Footer version chip + one-tap self-update (HMAC-signed OTM proxy with
  explicit field picking — foreign JSON can never reach the _setSession/_redirect
  control keys), owner-gated, 10-min sessionStorage cache on the check.

Hardening that rode along: session key now derived from OPERATOR_SHARED_SECRET
(managed boot REFUSES the old derivable DATABASE_URL fallback), 1MB JSON body
cap, login.html prototype-lookup fix. NOTE: server.mjs previously contained a
literal NUL byte that made git treat it as binary — this commit re-encodes it
as an escape (behavior identical) and adds .gitattributes so source diffs can
never go blind again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmybqyQmZWfcqA1vMP4jbQ
2026-08-09 22:31:34 -05:00

170 lines
4.5 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
passwordHash String
role String @default("OWNER")
created DateTime @default(now())
}
// spent OTM sign-in tokens (operator-magic + otm-sso) — jti is the single-use
// key; operatorEmail is the audit trail of who came in through OTM
model OperatorMagicConsumed {
jti String @id
operatorEmail String @default("")
consumedAt DateTime @default(now())
}
model Note {
id Int @id @default(autoincrement())
text String
tags String @default("")
created DateTime @default(now())
}
model Todo {
id Int @id @default(autoincrement())
text String
done Boolean @default(false)
started Boolean @default(false)
parentId Int?
created DateTime @default(now())
}
model Shopping {
id Int @id @default(autoincrement())
text String
done Boolean @default(false)
created DateTime @default(now())
}
model Glaze {
id Int @id @default(autoincrement())
name String
kind String @default("glaze")
cone String @default("")
atmosphere String @default("")
surface String @default("")
source String @default("mine")
sg String @default("")
swatch String @default("")
created DateTime @default(now())
materials GlazeMaterial[]
journal GlazeJournal[]
}
model GlazeMaterial {
id Int @id @default(autoincrement())
glazeId Int
glaze Glaze @relation(fields: [glazeId], references: [id], onDelete: Cascade)
name String
pct Float
addition Boolean @default(false)
}
model GlazeJournal {
id Int @id @default(autoincrement())
glazeId Int
glaze Glaze @relation(fields: [glazeId], references: [id], onDelete: Cascade)
text String
created DateTime @default(now())
}
model Firing {
id Int @id @default(autoincrement())
type String @default("glaze")
cone String @default("∆6")
schedule String @default("")
status String @default("firing")
resultNotes String @default("")
started DateTime @default(now())
unloaded DateTime?
items FiringItem[]
}
model FiringItem {
id Int @id @default(autoincrement())
firingId Int
firing Firing @relation(fields: [firingId], references: [id], onDelete: Cascade)
piece String
clay String @default("")
glaze String @default("")
rating String @default("")
note String @default("")
}
model Photo {
id Int @id @default(autoincrement())
entity String
entityId Int
filename String
created DateTime @default(now())
}
model FileAsset {
id Int @id @default(autoincrement())
entity String @default("")
entityId Int @default(0)
stored String
name String
tag String @default("")
size Int @default(0)
created DateTime @default(now())
}
model Product {
id Int @id @default(autoincrement())
name String
kind String @default("core line")
category String @default("")
price Float @default(0)
cost Float @default(0)
qty Int @default(0)
status String @default("in progress")
notes String @default("")
created DateTime @default(now())
sales Sale[]
}
model Sale {
id Int @id @default(autoincrement())
productId Int
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
qty Int @default(1)
price Float @default(0)
cost Float @default(0)
created DateTime @default(now())
}
model Supplier {
id Int @id @default(autoincrement())
name String
kind String @default("online")
distance String @default("")
url String @default("")
notes String @default("")
searchUrl String @default("")
created DateTime @default(now())
prices PriceEntry[]
}
model PriceEntry {
id Int @id @default(autoincrement())
item String
supplierId Int
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
price Float
unit String @default("")
shipping Float @default(0)
note String @default("")
created DateTime @default(now())
}