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 }