- Protocol 1 (docs/hub-protocol.md): public key, register, signed heartbeats - Ed25519 licence tokens (30-day validity, status, expiry, grace, limits) - Dashboard: installs, install detail, customers and licences, alerts, settings and audit log; two-factor login required - Alert checks every 5 minutes, emailed once; heartbeat roll-up after 30 days - Unit tests for encryption, tokens, signatures, TOTP and alert rules Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
150 lines
4.3 KiB
Plaintext
150 lines
4.3 KiB
Plaintext
// Hope Events Hub — customers, licences, installs and their heartbeats.
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
// Someone who logs in to the Hub (Joshua, later support staff). Two-factor (authenticator
|
|
// app) is required: totpSecret is set up on first login.
|
|
model AdminUser {
|
|
id String @id @default(uuid())
|
|
email String @unique
|
|
name String
|
|
password String
|
|
// AES-GCM encrypted (utils/crypto.js); null until two-factor is set up.
|
|
totpSecret String?
|
|
totpEnabled Boolean @default(false)
|
|
// Bumped to sign out everywhere.
|
|
tokenVersion Int @default(0)
|
|
lastLoginAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
auditLogs AuditLog[]
|
|
}
|
|
|
|
model Customer {
|
|
id String @id @default(uuid())
|
|
name String
|
|
contactName String?
|
|
email String?
|
|
phone String?
|
|
notes String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
licences Licence[]
|
|
installs Install[]
|
|
}
|
|
|
|
enum LicenceStatus {
|
|
active
|
|
suspended
|
|
revoked
|
|
}
|
|
|
|
model Licence {
|
|
id String @id @default(uuid())
|
|
customerId String
|
|
customer Customer @relation(fields: [customerId], references: [id], onDelete: Restrict)
|
|
// CEH-XXXX-XXXX-XXXX-XXXX, generated by the Hub.
|
|
key String @unique
|
|
plan String @default("standard")
|
|
status LicenceStatus @default(active)
|
|
// Null = no end date.
|
|
expiresAt DateTime?
|
|
graceDays Int @default(14)
|
|
// Hope Events 2.0: monthly AI spend cap (in cents, rand). 0 = AI off.
|
|
aiMonthlyCapCents Int @default(0)
|
|
notes String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
// One licence = one install.
|
|
install Install?
|
|
}
|
|
|
|
model Install {
|
|
id String @id @default(uuid())
|
|
customerId String
|
|
customer Customer @relation(fields: [customerId], references: [id], onDelete: Restrict)
|
|
licenceId String? @unique
|
|
licence Licence? @relation(fields: [licenceId], references: [id], onDelete: SetNull)
|
|
name String
|
|
url String
|
|
// The install's HMAC secret, AES-GCM encrypted: the Hub needs it to check signatures.
|
|
secret String
|
|
version String?
|
|
protocolVersion Int @default(1)
|
|
adminContact Json?
|
|
server Json?
|
|
timezone String?
|
|
features Json?
|
|
// From the latest heartbeat.
|
|
lastHeartbeatAt DateTime?
|
|
health Json?
|
|
stats Json?
|
|
notes String?
|
|
registeredAt DateTime @default(now())
|
|
// Set when Joshua releases the licence (e.g. the church moved domain): heartbeats are refused.
|
|
releasedAt DateTime?
|
|
heartbeats Heartbeat[]
|
|
events InstallEvent[]
|
|
|
|
@@index([lastHeartbeatAt])
|
|
}
|
|
|
|
// Every heartbeat for 30 days, then one per install per day.
|
|
model Heartbeat {
|
|
id String @id @default(uuid())
|
|
installId String
|
|
install Install @relation(fields: [installId], references: [id], onDelete: Cascade)
|
|
receivedAt DateTime @default(now())
|
|
version String?
|
|
health Json?
|
|
stats Json?
|
|
|
|
@@index([installId, receivedAt])
|
|
}
|
|
|
|
enum EventLevel {
|
|
info
|
|
warning
|
|
critical
|
|
}
|
|
|
|
// Things that happened to an install: alerts (offline, backup failing, disk low, errors,
|
|
// licence expiring), version changes, registration. An alert stays open (resolvedAt null)
|
|
// until the condition clears, so it's emailed once.
|
|
model InstallEvent {
|
|
id String @id @default(uuid())
|
|
installId String
|
|
install Install @relation(fields: [installId], references: [id], onDelete: Cascade)
|
|
type String
|
|
level EventLevel @default(info)
|
|
message String
|
|
createdAt DateTime @default(now())
|
|
resolvedAt DateTime?
|
|
|
|
@@index([installId, createdAt])
|
|
@@index([type, resolvedAt])
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(uuid())
|
|
actorId String?
|
|
actor AdminUser? @relation(fields: [actorId], references: [id], onDelete: SetNull)
|
|
action String
|
|
target String?
|
|
metadata Json?
|
|
ip String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model Setting {
|
|
key String @id
|
|
value String
|
|
}
|