Files
panel/controller/internal/db/steam_credentials.go
T
dbledeez 4ccccc6fe2 panel v0.9.2 — public release
Self-hostable game server control panel: Go controller + agent, 26 game
modules, embedded web UI. One-line install via install.sh / install.ps1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:43:35 -07:00

125 lines
4.5 KiB
Go

package db
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
)
// SteamCredentialRow is a single stored Steam account for SteamCMD
// non-anonymous login. v1 is single-operator, so most panels will have
// at most one row.
type SteamCredentialRow struct {
ID int64
Username string
PasswordCiphertext []byte // AES-GCM ciphertext of the password
PasswordNonce []byte // AES-GCM nonce (12 bytes)
SentryFile []byte // SSFN device-auth blob; NULL until first success
LastUsedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
// GetSteamCredentialByUsername returns the stored credential for the given
// username or pgx.ErrNoRows if none exists.
func (db *DB) GetSteamCredentialByUsername(ctx context.Context, username string) (*SteamCredentialRow, error) {
row := db.pool.QueryRow(ctx, `
SELECT id, username, password_ciphertext, password_nonce, sentry_file, last_used_at, created_at, updated_at
FROM steam_credentials
WHERE username = $1`, username)
var r SteamCredentialRow
if err := row.Scan(&r.ID, &r.Username, &r.PasswordCiphertext, &r.PasswordNonce, &r.SentryFile, &r.LastUsedAt, &r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
return &r, nil
}
// GetAnySteamCredential returns the most-recently-used Steam credential, or
// pgx.ErrNoRows if none exist. Used when the caller doesn't care which
// account (single-operator panels typically have one).
func (db *DB) GetAnySteamCredential(ctx context.Context) (*SteamCredentialRow, error) {
row := db.pool.QueryRow(ctx, `
SELECT id, username, password_ciphertext, password_nonce, sentry_file, last_used_at, created_at, updated_at
FROM steam_credentials
ORDER BY COALESCE(last_used_at, created_at) DESC
LIMIT 1`)
var r SteamCredentialRow
if err := row.Scan(&r.ID, &r.Username, &r.PasswordCiphertext, &r.PasswordNonce, &r.SentryFile, &r.LastUsedAt, &r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
return &r, nil
}
// UpsertSteamCredential stores (or replaces) a Steam credential. Leaves
// sentry_file untouched on update — that's populated separately by
// UpdateSteamSentryFile once a SteamCMD login succeeds.
func (db *DB) UpsertSteamCredential(ctx context.Context, username string, cipher, nonce []byte) error {
_, err := db.pool.Exec(ctx, `
INSERT INTO steam_credentials (username, password_ciphertext, password_nonce, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (username)
DO UPDATE SET password_ciphertext = EXCLUDED.password_ciphertext,
password_nonce = EXCLUDED.password_nonce,
updated_at = NOW()`,
username, cipher, nonce)
if err != nil {
return fmt.Errorf("upsert steam credential: %w", err)
}
return nil
}
// UpdateSteamSentryFile stores the SSFN blob after a successful login so
// subsequent SteamCMD invocations can skip the Steam Guard 2FA prompt.
func (db *DB) UpdateSteamSentryFile(ctx context.Context, username string, sentry []byte) error {
res, err := db.pool.Exec(ctx, `
UPDATE steam_credentials
SET sentry_file = $2, last_used_at = NOW(), updated_at = NOW()
WHERE username = $1`,
username, sentry)
if err != nil {
return fmt.Errorf("update sentry: %w", err)
}
if res.RowsAffected() == 0 {
return errors.New("steam credential not found")
}
return nil
}
// TouchSteamCredential updates last_used_at without writing a new sentry.
func (db *DB) TouchSteamCredential(ctx context.Context, username string) error {
_, err := db.pool.Exec(ctx, `UPDATE steam_credentials SET last_used_at = NOW() WHERE username = $1`, username)
return err
}
// DeleteSteamCredential removes a cached credential (operator action).
func (db *DB) DeleteSteamCredential(ctx context.Context, username string) error {
_, err := db.pool.Exec(ctx, `DELETE FROM steam_credentials WHERE username = $1`, username)
return err
}
// ListSteamCredentialUsernames returns just the usernames (no password
// material) so the UI can show "Steam accounts configured: X" without
// touching the secrets.
func (db *DB) ListSteamCredentialUsernames(ctx context.Context) ([]string, error) {
rows, err := db.pool.Query(ctx, `SELECT username FROM steam_credentials ORDER BY username`)
if err != nil {
return nil, err
}
defer rows.Close()
var names []string
for rows.Next() {
var u string
if err := rows.Scan(&u); err != nil {
return nil, err
}
names = append(names, u)
}
return names, rows.Err()
}
// avoid unused-import removal
var _ = pgx.ErrNoRows