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
170 lines
4.5 KiB
Plaintext
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())
|
|
}
|