vibes
This commit is contained in:
+157
@@ -0,0 +1,157 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user