Files
2026-10-01 22:16:48 +02:00

158 lines
4.7 KiB
Go

package store
import (
"context"
"database/sql"
"strings"
)
// AuditEntry is one thing a player did, recorded for the operator.
type AuditEntry struct {
At int64 // unix seconds
PlayerID int64 // the account that acted
Action string // register, login, code_save, code_clear, program_upload, launch, scrap, uplink
Ship int64 // the ship concerned, or 0
Slot int // the code slot concerned, or 0
Detail string // JSON with the specifics
IP string
}
// AuditRow is an entry as listed, with its id and the account's name.
type AuditRow struct {
AuditEntry
ID int64
Name string
}
// AddAudit appends an entry to the audit log.
func (q *Q) AddAudit(ctx context.Context, e AuditEntry) error {
_, err := q.exec(ctx, `INSERT INTO audit_log (at, player_id, action, ship_id, slot, detail, ip) VALUES (?, ?, ?, ?, ?, ?, ?)`,
e.At, e.PlayerID, e.Action, e.Ship, e.Slot, e.Detail, e.IP)
return err
}
// AuditFilter narrows ListAudit. Zero values mean "any".
type AuditFilter struct {
PlayerID int64
Action string
Before int64 // only entries with an id below this, for paging back through the log
Limit int
}
// ListAudit returns entries newest first.
func (q *Q) ListAudit(ctx context.Context, f AuditFilter) ([]AuditRow, error) {
if f.Limit <= 0 || f.Limit > 500 {
f.Limit = 100
}
rows, err := q.query(ctx, `SELECT a.id, a.at, a.player_id, COALESCE(p.name, ''), a.action, a.ship_id, a.slot, a.detail, a.ip
FROM audit_log a LEFT JOIN players p ON p.id = a.player_id
WHERE (? = 0 OR a.player_id = ?) AND (? = '' OR a.action = ?) AND (? = 0 OR a.id < ?)
ORDER BY a.id DESC LIMIT ?`,
f.PlayerID, f.PlayerID, f.Action, f.Action, f.Before, f.Before, f.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
out := []AuditRow{}
for rows.Next() {
var r AuditRow
if err := rows.Scan(&r.ID, &r.At, &r.PlayerID, &r.Name, &r.Action, &r.Ship, &r.Slot, &r.Detail, &r.IP); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
// AccountRow summarises one account for the admin list.
type AccountRow struct {
ID int64
Name string
Credits int64
Ships int
Active int // ships in the belt
Slots int // code slots in use
Actions int // audit entries
LastLogin int64
LastAction int64
}
// ListAccounts returns every account, oldest first.
func (q *Q) ListAccounts(ctx context.Context) ([]AccountRow, error) {
rows, err := q.query(ctx, `SELECT p.id, p.name, p.credits,
(SELECT COUNT(*) FROM ships s WHERE s.owner_id = p.id),
(SELECT COUNT(*) FROM ships s WHERE s.owner_id = p.id AND s.status = ?),
(SELECT COUNT(*) FROM code_slots c WHERE c.player_id = p.id),
(SELECT COUNT(*) FROM audit_log a WHERE a.player_id = p.id),
(SELECT MAX(a.at) FROM audit_log a WHERE a.player_id = p.id AND a.action = 'login'),
(SELECT MAX(a.at) FROM audit_log a WHERE a.player_id = p.id)
FROM players p ORDER BY p.id`, StatusActive)
if err != nil {
return nil, err
}
defer rows.Close()
out := []AccountRow{}
for rows.Next() {
var r AccountRow
var login, last sql.NullInt64
if err := rows.Scan(&r.ID, &r.Name, &r.Credits, &r.Ships, &r.Active, &r.Slots, &r.Actions, &login, &last); err != nil {
return nil, err
}
r.LastLogin, r.LastAction = login.Int64, last.Int64
out = append(out, r)
}
return out, rows.Err()
}
// PlayerByID looks an account up by number.
func (q *Q) PlayerByID(ctx context.Context, id int64) (Player, error) {
var p Player
err := q.row(ctx, `SELECT id, name, credits FROM players WHERE id = ?`, id).Scan(&p.ID, &p.Name, &p.Credits)
if err == sql.ErrNoRows {
return p, ErrNotFound
}
return p, err
}
// SlotSummary describes a saved code slot without its source.
type SlotSummary struct {
Slot int
Name string
Size int
UpdatedAt string
}
// ListSlotSummaries returns a player's saved slots for the admin view.
func (q *Q) ListSlotSummaries(ctx context.Context, owner int64) ([]SlotSummary, error) {
rows, err := q.query(ctx, `SELECT slot, name, LENGTH(source), updated_at FROM code_slots WHERE player_id = ? ORDER BY slot`, owner)
if err != nil {
return nil, err
}
defer rows.Close()
out := []SlotSummary{}
for rows.Next() {
var s SlotSummary
if err := rows.Scan(&s.Slot, &s.Name, &s.Size, &s.UpdatedAt); err != nil {
return nil, err
}
out = append(out, s)
}
return out, rows.Err()
}
// AuditActions lists the action names the log uses.
var AuditActions = []string{"register", "login", "code_save", "code_clear", "program_upload", "launch", "scrap", "uplink"}
// ValidAuditAction reports whether name is one of AuditActions (or empty, meaning any).
func ValidAuditAction(name string) bool {
if name == "" {
return true
}
for _, a := range AuditActions {
if strings.EqualFold(a, name) {
return true
}
}
return false
}