158 lines
4.7 KiB
Go
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
|
|
}
|