Adds the player-facing half of game account management: a panel on /community/servers/[id] that shows your linked account for that server, creates one, and changes its password. None of these emulators speak OIDC and none will - each ships its own login against its own auth database. So this is a bridge, not SSO: one Drop identity tied to one native account per server, enforced by a unique index on (userId, serverSlug). That constraint IS the feature. Drop owns identity, authorization, the link table and the UI. The per-game DB access and password crypto stay in mmo-portal's provider API (SRP6 verifiers, EQEmu hashing) - code that writes into live auth DBs and fails silently when subtly wrong. The password never touches Drop's database; it goes straight through to the provider, which turns it into whatever that emulator wants. Notable details: - GameServer.slug names the game, provider.key names the emulator. They agree for eqemu and daoc but wow != trinitycore, so the mapping is an explicit three-entry constant, not an assumption. - Panel renders NOTHING when a server has no account backend (Impostor is a lobby with no identity), rather than a dead button. - 'backend unreachable' is kept distinct from 'you have no account' - the latter would invite a duplicate during an outage. - Provision creates in the game first, then links. The reverse would leave a link pointing at a nonexistent account, which reads as corruption to every later call. - Authorization is just an authenticated Drop session with the ACL: Drop access and game access already imply each other, so a third gate could only disagree with them. Typecheck clean.
281 lines
8.6 KiB
Text
281 lines
8.6 KiB
Text
// --- M4 --- game-server registry and issue reports (drop-community M4).
|
|
//
|
|
// Ownership: GameServer, GameServerStatusHistory, Issue, and IssueComment are
|
|
// this milestone's models. Cross-references to User (reporter/resolver/
|
|
// owner) are deliberately PLAIN string id fields with no Prisma @relation,
|
|
// matching the existing `profilePictureObjectId String // Object` precedent
|
|
// in user.prisma: it avoids editing the shared User model (and the back-
|
|
// relation array Prisma would otherwise require there) while three other
|
|
// milestones are concurrently adding their own models in this same repo.
|
|
// Joins against User happen in the service layer instead.
|
|
//
|
|
// Title spine note: this fork's title spine is split, not unified (see
|
|
// community.prisma's header) -- Drop content is a `Game` row directly, RomM
|
|
// content is a `CommunityTitle` row, and some servers (EQEmu, the WoW realm,
|
|
// the Goldberg relay) are not tied to any specific title at all. So instead
|
|
// of an FK to one table, both GameServer and Issue carry a
|
|
// (titlePlatform, titleId) pair -- the same (platform, external_id) idea
|
|
// DESIGN.md's unified `title` table used, applied across two real tables
|
|
// (plus "none") instead of one. No formal relation, for the same reason as
|
|
// the User references above.
|
|
enum CommunityContentPlatform {
|
|
drop
|
|
romm
|
|
none
|
|
}
|
|
|
|
enum GameServerKind {
|
|
eqemu
|
|
wow
|
|
goldberg_relay
|
|
minecraft
|
|
source
|
|
generic
|
|
}
|
|
|
|
enum GameServerProtocol {
|
|
tcp
|
|
udp
|
|
http
|
|
}
|
|
|
|
enum GameServerStatus {
|
|
up
|
|
down
|
|
degraded
|
|
unknown
|
|
}
|
|
|
|
// Strategy-per-probe_type, mirroring script-library's
|
|
// game-server-registry-probe.py exactly (the reference implementation this
|
|
// milestone's ServerProbe sweep is built from). spire_api and source_query
|
|
// are modeled now so a server can declare them without a schema change, but
|
|
// have no working implementation yet -- they report "unknown", never a
|
|
// guessed status.
|
|
enum GameServerProbeType {
|
|
tcp_connect
|
|
http_get
|
|
spire_api
|
|
presence_inferred
|
|
source_query
|
|
none
|
|
}
|
|
|
|
enum GameServerRegistration {
|
|
manual
|
|
auto
|
|
}
|
|
|
|
// DESIGN.md's game_server.visibility also has a `friends` value, deferred
|
|
// here: friends-only visibility needs the Friendship graph, which is M3's
|
|
// model, not merged into this branch. Adding `friends` later is a single
|
|
// additive enum value (ALTER TYPE ... ADD VALUE), so this is not a dead end.
|
|
enum GameServerVisibility {
|
|
public
|
|
private
|
|
}
|
|
|
|
model GameServer {
|
|
id String @id @default(uuid())
|
|
name String
|
|
|
|
// Stable, hand-picked slug -- NOT a Prisma relation, same reasoning as
|
|
// ChatRoom.serverKey/gameServerId (see community.prisma): M3's chat
|
|
// shipped three server-scoped rooms (KNOWN_SERVERS in chatService.ts,
|
|
// keyed "eqemu" | "wow" | "daoc") before this milestone landed, and
|
|
// reserved `ChatRoom.gameServerId` for a future backfill pass matched by
|
|
// slug equality. These three rows use exactly those keys so that match is
|
|
// trivial: WHERE "GameServer"."slug" = "ChatRoom"."serverKey". Do not
|
|
// rename an existing slug without also updating chatService.ts's
|
|
// KNOWN_SERVERS and any backfill that has already run.
|
|
slug String @unique
|
|
|
|
// Persistent-world servers where "is anyone on right now" is the whole
|
|
// question (WoW, EQEmu, OpenDAoC) get pinned to the top of the compact
|
|
// status panel on the community landing page, ahead of one-off/lobby
|
|
// servers (Impostor) that share the same `kind: generic` bucket and
|
|
// otherwise couldn't be told apart from it. Admin-settable per server,
|
|
// not hardcoded by name in the UI.
|
|
featured Boolean @default(false)
|
|
|
|
titlePlatform CommunityContentPlatform @default(none)
|
|
titleId String?
|
|
|
|
kind GameServerKind
|
|
host String
|
|
port Int
|
|
protocol GameServerProtocol
|
|
|
|
status GameServerStatus @default(unknown)
|
|
playersOnline Int?
|
|
lastCheckedAt DateTime?
|
|
lastUpAt DateTime?
|
|
consecutiveFailures Int @default(0)
|
|
|
|
// probeConfig mirrors the registry JSON's `probe_config` shape per
|
|
// probe_type, plus an optional nested `playerCountProbe` object
|
|
// ({method, ...}) for the additive, non-gating player-count extensions
|
|
// (ssh_mysql_query, ssh_docker_logs_regex) documented in
|
|
// script-library/scripts/game-server-registry-probe.py. Never used to
|
|
// compute `status` -- only probeType is.
|
|
probeType GameServerProbeType @default(none)
|
|
probeConfig Json?
|
|
probeIntervalSec Int @default(60)
|
|
|
|
joinInstructions String?
|
|
deepLink String?
|
|
|
|
registration GameServerRegistration @default(manual)
|
|
ownerUserId String?
|
|
visibility GameServerVisibility @default(public)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
statusHistory GameServerStatusHistory[]
|
|
|
|
@@index([kind])
|
|
@@index([status])
|
|
@@index([featured])
|
|
@@index([titlePlatform, titleId])
|
|
}
|
|
|
|
model GameServerStatusHistory {
|
|
id String @id @default(uuid())
|
|
serverId String
|
|
server GameServer @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
|
|
|
checkedAt DateTime @default(now())
|
|
status GameServerStatus
|
|
latencyMs Int?
|
|
playersOnline Int?
|
|
detail Json?
|
|
|
|
@@index([serverId, checkedAt(sort: Desc)])
|
|
}
|
|
|
|
enum IssueKind {
|
|
bug
|
|
crash
|
|
launch_failure
|
|
missing_files
|
|
multiplayer
|
|
performance
|
|
request
|
|
}
|
|
|
|
enum IssueSeverity {
|
|
blocker
|
|
major
|
|
minor
|
|
cosmetic
|
|
}
|
|
|
|
enum IssueStatus {
|
|
open
|
|
triaged
|
|
in_progress
|
|
resolved
|
|
wontfix
|
|
duplicate
|
|
}
|
|
|
|
// Generic over platform for free, because titlePlatform/titleId already
|
|
// abstracts drop | romm: a RomM DOS title and a Drop Steam title file into
|
|
// the same queue with the same code (DESIGN.md sect. 7.7).
|
|
model Issue {
|
|
id String @id @default(uuid())
|
|
|
|
titlePlatform CommunityContentPlatform
|
|
titleId String
|
|
|
|
reporterId String
|
|
|
|
kind IssueKind
|
|
severity IssueSeverity @default(minor)
|
|
|
|
subject String
|
|
body String
|
|
|
|
status IssueStatus @default(open)
|
|
resolvedById String?
|
|
resolvedAt DateTime?
|
|
resolutionNote String?
|
|
|
|
duplicateOfId String?
|
|
duplicateOf Issue? @relation("IssueDuplicates", fields: [duplicateOfId], references: [id])
|
|
duplicates Issue[] @relation("IssueDuplicates")
|
|
|
|
// Auto-attached by the launch wrapper once M2's wrapper surface exists
|
|
// (OS build, GPU, wrapper version, resolved launch command, exit code,
|
|
// log tail). Optional and unused until then; a manually filed issue just
|
|
// leaves this null.
|
|
environment Json?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
comments IssueComment[]
|
|
|
|
@@index([titlePlatform, titleId])
|
|
@@index([status])
|
|
@@index([reporterId])
|
|
}
|
|
|
|
model IssueComment {
|
|
id String @id @default(uuid())
|
|
|
|
issueId String
|
|
issue Issue @relation(fields: [issueId], references: [id], onDelete: Cascade)
|
|
|
|
authorId String
|
|
body String
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([issueId, createdAt])
|
|
}
|
|
|
|
// --- M6: SSO identity <-> native game account ------------------------------
|
|
//
|
|
// THE WHOLE POINT: none of these emulators speak OIDC and none of them will.
|
|
// EQEmu, TrinityCore and OpenDAoC each ship their own login against their own
|
|
// auth database. So this is not SSO for the game servers -- it is a BRIDGE:
|
|
// one Drop identity tied to one native game account per server, so a player
|
|
// has a single place to create and reset credentials they will then type into
|
|
// a game client that has never heard of Authentik.
|
|
// See docs/design/0002-game-account-management.md.
|
|
//
|
|
// ONE ACCOUNT PER USER PER SERVER is the feature, not a limitation -- "tie my
|
|
// identity to my account on that server" only means one thing. Enforced by
|
|
// @@unique([userId, serverSlug]).
|
|
//
|
|
// serverSlug is GameServer.slug, NOT a relation -- same reasoning as
|
|
// ChatRoom.serverKey above. It also deliberately survives a GameServer row
|
|
// being deleted and re-added: the link is to the SERVER as a concept, and
|
|
// losing a player's account mapping because a registry row was recreated
|
|
// would be data loss with no upside.
|
|
//
|
|
// accountId is provider-defined and OPAQUE -- TEXT even where the game's
|
|
// native id is numeric (TrinityCore's account.id). That is what lets one
|
|
// column work for every backend; mmo-portal's own link table made the same
|
|
// call for the same reason. Providers convert at their own boundary.
|
|
//
|
|
// NEVER INFERRED FROM A NAME MATCH. Every row is written explicitly at
|
|
// provisioning time. A game account whose username happens to equal a Drop
|
|
// username is not that user's account.
|
|
model GameAccountLink {
|
|
id String @id @default(uuid())
|
|
|
|
userId String
|
|
serverSlug String
|
|
|
|
accountId String
|
|
accountUsername String
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([userId, serverSlug])
|
|
@@index([serverSlug])
|
|
@@index([userId])
|
|
}
|