package api import ( "context" "time" ) // ServerRecord is the business-layer projection of a server (spec §6 servers / // server_aliases). It carries only what the CRD cannot express — ownership and // the cached subdomain alias — never authoritative lifecycle fields. type ServerRecord struct { Name string Subdomain string // OwnerID is the claiming user, or "" when the server is unclaimed. OwnerID string // CachedPhase is the non-authoritative phase projection used for fast lists. CachedPhase string } // MyServerView is a row of GET /api/v1/me/servers: a server the caller owns, // may auto-start, or may claim. type MyServerView struct { Name string `json:"name"` Subdomain string `json:"subdomain"` Owned bool `json:"owned"` Claimable bool `json:"claimable"` Phase string `json:"phase,omitempty"` } // AuditEntry is one row written to audit_logs (spec §6). The actor is the Access // email for human callers and the component name for internal callers. type AuditEntry struct { Actor string Source string Action string ServerName string RequestID string } // BackupView is one row of GET /api/v1/backups (spec §7, world_backups in §22). // It deliberately omits backup_ref — the opaque internal storage handle (a tar // path / VolumeSnapshot name / Longhorn URL, spec §19) — on the same principle // that keeps the RCON password server-side (spec §286): the client lists backups // and triggers a restore by server, never by handle. type BackupView struct { ID string `json:"id"` ServerName string `json:"server_name"` FormerOwner string `json:"former_owner,omitempty"` SizeBytes int64 `json:"size_bytes"` Reason string `json:"reason"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` ExpiresAt time.Time `json:"expires_at"` } // BackupRecord is the server-side view of a backup used to drive a restore (spec // §466). It carries the opaque backup_ref the Restorer needs and the former_owner // the restore authorization compares against — neither is ever serialized to the // client (contrast BackupView). type BackupRecord struct { ID string ServerName string FormerOwner string BackupRef string SizeBytes int64 } // Repo is the business-layer data access the API depends on. It is an interface // so handlers are tested against an in-memory fake; the Postgres implementation // (pgRepo) is integration-tested only — it requires a live database. type Repo interface { // ServerBySubdomain resolves a subdomain alias to its server, or ErrNotFound. ServerBySubdomain(ctx context.Context, subdomain string) (*ServerRecord, error) // ServerByName loads a server's business projection, or ErrNotFound. ServerByName(ctx context.Context, name string) (*ServerRecord, error) // IsLinked reports whether the user has a verified MC account link (spec §10), // a precondition for every ownership operation. IsLinked(ctx context.Context, userID string) (bool, error) // CreateLinkCode mints a one-time account-link code for an in-game player // (spec §10: 游戏内 /link → 生成一次性码). The code is born knowing only the // verified mc_uuid (online-mode=true established it); a web user binds it to // their user_id later via VerifyLinkCode. This is internal-face only — the web // has no verified UUID to mint against (the account_link_codes schema has no // user_id column, which forces the in-game origin). CreateLinkCode(ctx context.Context, code, mcUUID string, expiresAt time.Time) error // VerifyLinkCode consumes a non-expired code for the logged-in user and writes // the account_links binding, atomically (spec §10: 网页 verify 填码 → 写 // account_links). It returns the bound mc_uuid. A missing or expired code → // ErrLinkCodeInvalid; a uuid already linked to a *different* user → ErrConflict; // re-verifying the same (user, uuid) pair is idempotent. now is the API clock so // expiry is testable. VerifyLinkCode(ctx context.Context, userID, code string, now time.Time) (mcUUID string, err error) // QuotaAvailable reports whether the user is under their max_servers quota // (spec §9.3 step ②, evaluated before provisioning). QuotaAvailable(ctx context.Context, userID string) (bool, error) // ClaimServer atomically sets owner_id where it is currently NULL and returns // whether a row changed. false means the server was already claimed (spec §9.3: // 0 rows → 409). ClaimServer(ctx context.Context, name, userID string) (bool, error) // UserInAllowlist reports whether the user's linked UUID is on the server // allowlist (spec §9.4). UserInAllowlist(ctx context.Context, name, userID string) (bool, error) // UUIDInAllowlist reports whether an in-game UUID is on the server allowlist // (spec §9.4). It is the internal-face symmetric of UserInAllowlist: velocity // drives a domain-autostart wake knowing the joining player only by their // online-mode UUID, never a web user_id, so the allowlist gate for that path // must be keyed by UUID directly (the allowlist table is UUID-keyed; the // account_links join in UserInAllowlist only exists to bridge the web side). UUIDInAllowlist(ctx context.Context, name, mcUUID string) (bool, error) // UserByMCUUID resolves a verified in-game UUID to the user_id it is linked to // (spec §10 account_links), or ErrNotFound when the UUID is not linked. The // internal-face wake uses it to apply the owner bypass for a player known only // by UUID; an unlinked UUID simply falls through to the autostartPolicy gate. UserByMCUUID(ctx context.Context, mcUUID string) (userID string, err error) // RecordJoin updates last_active_at, clears reaper warnings, and auto-appends // the UUID to the allowlist (spec §7 join-event, §9.4). RecordJoin(ctx context.Context, name, mcUUID string) error // MyServers lists the servers a user owns or may claim. MyServers(ctx context.Context, userID string) ([]MyServerView, error) // AllBackups lists every present world backup, newest first (spec §7 GET // /backups, admin scope). Expired/deleted rows are never returned. AllBackups(ctx context.Context) ([]BackupView, error) // BackupsForUser lists the present world backups of worlds the user formerly // owned, newest first (spec §7 GET /backups, former_owner scope). The // former_owner column is stamped when the world is archived at release time, so // a user sees their own released worlds even after the server is re-seeded. BackupsForUser(ctx context.Context, userID string) ([]BackupView, error) // LatestBackup returns the most recent present backup for a server, or // ErrNotFound when none exists (spec §466 restore). The returned BackupRecord // carries the server-side backup_ref + former_owner the restore path needs; the // client never sees them. LatestBackup(ctx context.Context, serverName string) (*BackupRecord, error) // SeedServer inserts the business-layer rows for a newly created server (spec // §15): a servers row (owner_id NULL — claimed later, spec §9.3) and its // subdomain alias, both idempotent. It returns ErrConflict if the subdomain is // already bound to a different server, so the create handler can fail before // touching the CRD. ClaimServer requires this row to exist, so a CRD-only // server would be unclaimable — the create path must seed here first. SeedServer(ctx context.Context, name, subdomain string) error // Audit appends one audit row. Audit(ctx context.Context, e AuditEntry) error }