skyrama_tui/internal/db/admin_client.go

548 lines
17 KiB
Go
Raw Permalink Normal View History

2026-06-04 20:51:20 +03:00
package db
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
"golang.org/x/crypto/bcrypt"
)
// AdminClient provides raw-SQL operations for the admin TUI.
type AdminClient struct {
db *sql.DB
ctx context.Context
}
// NewAdminClient opens and pings a MySQL connection.
func NewAdminClient(dsn string) (*AdminClient, error) {
d, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
d.SetMaxOpenConns(5)
d.SetMaxIdleConns(3)
d.SetConnMaxLifetime(5 * time.Minute)
ctx := context.Background()
if err := d.PingContext(ctx); err != nil {
d.Close()
return nil, fmt.Errorf("ping db: %w", err)
}
return &AdminClient{db: d, ctx: ctx}, nil
}
func (c *AdminClient) Close() { c.db.Close() }
// ── Products ──────────────────────────────────────────────────────────────────
type AdminProduct struct {
ID uint16
Name string
}
func (c *AdminClient) ListProducts() ([]AdminProduct, error) {
rows, err := c.db.QueryContext(c.ctx, `SELECT id, name FROM products ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminProduct
for rows.Next() {
var p AdminProduct
if err := rows.Scan(&p.ID, &p.Name); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
func (c *AdminClient) AddProduct(name string) error {
_, err := c.db.ExecContext(c.ctx, `INSERT INTO products (name) VALUES (?)`, name)
return err
}
func (c *AdminClient) DeleteProduct(id uint16) error {
_, err := c.db.ExecContext(c.ctx, `DELETE FROM products WHERE id = ?`, id)
return err
}
// ── Countries ─────────────────────────────────────────────────────────────────
type AdminCountry struct {
ID uint16
Name string
Continent string
P1, P2, P3 uint16
}
func (c *AdminClient) ListCountries() ([]AdminCountry, error) {
rows, err := c.db.QueryContext(c.ctx,
`SELECT id, name, continent, produce_1_id, produce_2_id, produce_3_id FROM countries ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminCountry
for rows.Next() {
var co AdminCountry
if err := rows.Scan(&co.ID, &co.Name, &co.Continent, &co.P1, &co.P2, &co.P3); err != nil {
return nil, err
}
out = append(out, co)
}
return out, rows.Err()
}
func (c *AdminClient) AddCountry(name, continent string, p1, p2, p3 uint16) error {
_, err := c.db.ExecContext(c.ctx,
`INSERT INTO countries (name,continent,produce_1_id,produce_2_id,produce_3_id) VALUES (?,?,?,?,?)`,
name, continent, p1, p2, p3)
return err
}
// ── Factions ──────────────────────────────────────────────────────────────────
type AdminFaction struct {
ID uint32
Name string
Continent string
Leader string
Background string
UserID int32
CentralAirportID uint32
}
func (c *AdminClient) ListFactions() ([]AdminFaction, error) {
rows, err := c.db.QueryContext(c.ctx,
`SELECT id, name, continent, leader_name, COALESCE(background,''), user_id, central_airport_id
FROM factions ORDER BY continent`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminFaction
for rows.Next() {
var f AdminFaction
if err := rows.Scan(&f.ID, &f.Name, &f.Continent, &f.Leader, &f.Background,
&f.UserID, &f.CentralAirportID); err != nil {
return nil, err
}
out = append(out, f)
}
return out, rows.Err()
}
func (c *AdminClient) AddFaction(name, continent, leader, background string, userID int32, centralAirportID uint32) error {
_, err := c.db.ExecContext(c.ctx,
`INSERT INTO factions (name, continent, leader_name, background, user_id, central_airport_id)
VALUES (?,?,?,?,?,?)`,
name, continent, leader, background, userID, centralAirportID)
return err
}
func (c *AdminClient) UpdateFaction(id uint32, name, continent, leader, background string, userID int32, centralAirportID uint32) error {
_, err := c.db.ExecContext(c.ctx,
`UPDATE factions SET name=?, continent=?, leader_name=?, background=?, user_id=?, central_airport_id=?
WHERE id=?`,
name, continent, leader, background, userID, centralAirportID, id)
return err
}
func (c *AdminClient) DeleteFaction(id uint32) error {
_, err := c.db.ExecContext(c.ctx, `DELETE FROM factions WHERE id = ?`, id)
return err
}
// ── Buildings ─────────────────────────────────────────────────────────────────
type AdminBuilding struct {
ID uint32
Name string
MinLevel uint32
Cost uint32
BuildingType string
Details string
// raw values used by the edit form
Capacity int
StorageType string
StorageSubtype string
OperationType string
OperationSize string
PassiveType string
PassiveRate int
}
func (c *AdminClient) ListBuildings() ([]AdminBuilding, error) {
rows, err := c.db.QueryContext(c.ctx, `
SELECT id, name, min_level, construction_cost_cash, building_type,
COALESCE(storage_type,''), COALESCE(operation_type,''), COALESCE(passive_type,''),
COALESCE(capacity,0), COALESCE(passive_rate,0), COALESCE(operation_size,''),
COALESCE(storage_subtype,'')
FROM buildings ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminBuilding
for rows.Next() {
var b AdminBuilding
if err := rows.Scan(&b.ID, &b.Name, &b.MinLevel, &b.Cost, &b.BuildingType,
&b.StorageType, &b.OperationType, &b.PassiveType,
&b.Capacity, &b.PassiveRate, &b.OperationSize, &b.StorageSubtype); err != nil {
return nil, err
}
switch b.BuildingType {
case "storage":
b.Details = fmt.Sprintf("cap:%d type:%s", b.Capacity, b.StorageType)
case "operation":
b.Details = fmt.Sprintf("%s/%s", b.OperationType, b.OperationSize)
case "passive":
b.Details = fmt.Sprintf("%s +%d/s", b.PassiveType, b.PassiveRate)
}
out = append(out, b)
}
return out, rows.Err()
}
type AddBuildingParams struct {
Name string
MinLevel uint32
Cost uint32
BuildingType string
Capacity int
StorageType string
StorageSubtype string
OperationType string
OperationSize string
PassiveType string
PassiveRate int
}
func (c *AdminClient) AddBuilding(p AddBuildingParams) error {
switch p.BuildingType {
case "storage":
_, err := c.db.ExecContext(c.ctx,
`INSERT INTO buildings (name,min_level,construction_cost_cash,building_type,capacity,storage_type,storage_subtype)
VALUES (?,?,?,'storage',?,?,?)`,
p.Name, p.MinLevel, p.Cost, p.Capacity, p.StorageType, p.StorageSubtype)
return err
case "operation":
_, err := c.db.ExecContext(c.ctx,
`INSERT INTO buildings (name,min_level,construction_cost_cash,building_type,operation_type,operation_size)
VALUES (?,?,?,'operation',?,?)`,
p.Name, p.MinLevel, p.Cost, p.OperationType, p.OperationSize)
return err
case "passive":
_, err := c.db.ExecContext(c.ctx,
`INSERT INTO buildings (name,min_level,construction_cost_cash,building_type,passive_type,passive_rate)
VALUES (?,?,?,'passive',?,?)`,
p.Name, p.MinLevel, p.Cost, p.PassiveType, p.PassiveRate)
return err
default:
_, err := c.db.ExecContext(c.ctx,
`INSERT INTO buildings (name,min_level,construction_cost_cash,building_type) VALUES (?,?,?,'none')`,
p.Name, p.MinLevel, p.Cost)
return err
}
}
func (c *AdminClient) DeleteBuilding(id uint32) error {
_, err := c.db.ExecContext(c.ctx, `DELETE FROM buildings WHERE id = ?`, id)
return err
}
// ── Planes ────────────────────────────────────────────────────────────────────
type AdminPlane struct {
ID uint32
Name string
Size string
AircraftType string
OperationType string
MinLevel uint32
TravelSec uint32
FuelCost uint32
BuyCost uint32
// extra fields for edit form
CashCost uint32
MaxPassengers int
XP uint32
CashReward int
CargoReward int
BoardingTimeSec uint32
FuelingTimeSec uint32
}
func (c *AdminClient) ListPlanes() ([]AdminPlane, error) {
rows, err := c.db.QueryContext(c.ctx, `
SELECT id, name, aircraft_size, aircraft_type, operation_type, min_level,
travel_time_inSeconds, operation_cost_fuel, buying_cost_cash,
operation_cost_cash, COALESCE(max_passengers,0),
completed_flight_experience,
COALESCE(completed_flight_cash_reward,0),
COALESCE(completed_flight_cargo_reward,0),
boarding_time_seconds, fueling_time_seconds
FROM planes ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminPlane
for rows.Next() {
var p AdminPlane
if err := rows.Scan(&p.ID, &p.Name, &p.Size, &p.AircraftType, &p.OperationType,
&p.MinLevel, &p.TravelSec, &p.FuelCost, &p.BuyCost,
&p.CashCost, &p.MaxPassengers, &p.XP, &p.CashReward, &p.CargoReward,
&p.BoardingTimeSec, &p.FuelingTimeSec); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}
type AddPlaneParams struct {
Name string
Size string
AircraftType string
OperationType string
MinLevel uint32
FuelCost uint32
CashCost uint32
MaxPassengers int
TravelSec uint32
BuyCost uint32
XP uint32
CashReward int
CargoReward int
BoardingTimeSec int
FuelingTimeSec int
}
func (c *AdminClient) AddPlane(p AddPlaneParams) error {
var maxPax, cashRew, cargoRew interface{}
if p.MaxPassengers > 0 {
maxPax = p.MaxPassengers
}
if p.CashReward > 0 {
cashRew = p.CashReward
}
if p.CargoReward > 0 {
cargoRew = p.CargoReward
}
_, err := c.db.ExecContext(c.ctx, `
INSERT INTO planes
(name,aircraft_size,operation_type,aircraft_type,min_level,operation_cost_fuel,
operation_cost_cash,max_passengers,travel_time_inSeconds,buying_cost_cash,
completed_flight_experience,completed_flight_cash_reward,completed_flight_cargo_reward,
boarding_time_seconds,fueling_time_seconds)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
p.Name, p.Size, p.OperationType, p.AircraftType, p.MinLevel,
p.FuelCost, p.CashCost, maxPax, p.TravelSec, p.BuyCost, p.XP, cashRew, cargoRew,
p.BoardingTimeSec, p.FuelingTimeSec)
return err
}
func (c *AdminClient) DeletePlane(id uint32) error {
_, err := c.db.ExecContext(c.ctx, `DELETE FROM planes WHERE id = ?`, id)
return err
}
// ── Users ─────────────────────────────────────────────────────────────────────
type AdminUser struct {
ID int32
Username string
CreatedAt time.Time
Deleted bool
}
func (c *AdminClient) ListUsers() ([]AdminUser, error) {
rows, err := c.db.QueryContext(c.ctx,
`SELECT id, username, created_at, (deleted_at IS NOT NULL) FROM users WHERE is_npc = FALSE ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminUser
for rows.Next() {
var u AdminUser
if err := rows.Scan(&u.ID, &u.Username, &u.CreatedAt, &u.Deleted); err != nil {
return nil, err
}
out = append(out, u)
}
return out, rows.Err()
}
func (c *AdminClient) CreateUserWithAirport(username, password, airportName string, countryID int) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
tx, err := c.db.BeginTx(c.ctx, nil)
if err != nil {
return err
}
res, err := tx.ExecContext(c.ctx,
`INSERT INTO users (username, password_hash) VALUES (?, ?)`, username, string(hash))
if err != nil {
tx.Rollback()
return err
}
userID, _ := res.LastInsertId()
_, err = tx.ExecContext(c.ctx,
`INSERT INTO airports (name, user_id, origin_country_id, cash) VALUES (?, ?, ?, 10000)`,
airportName, userID, countryID)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
func (c *AdminClient) DeleteUser(username string) error {
_, err := c.db.ExecContext(c.ctx,
`UPDATE users SET deleted_at = CURRENT_TIMESTAMP WHERE username = ?`, username)
return err
}
// ── Airports ──────────────────────────────────────────────────────────────────
type AdminAirport struct {
ID uint32
Name string
Username string
Cash uint64
Fuel uint32
Level uint32
MaxFuel uint32
MaxPax uint32
}
func (c *AdminClient) ListAirports() ([]AdminAirport, error) {
rows, err := c.db.QueryContext(c.ctx, `
SELECT a.id, a.name, u.username, a.cash, a.current_fuel, a.player_level,
a.max_fuel_capacity, a.max_passenger_capacity
FROM airports a JOIN users u ON a.user_id = u.id
WHERE a.is_deleted = FALSE ORDER BY a.id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminAirport
for rows.Next() {
var a AdminAirport
if err := rows.Scan(&a.ID, &a.Name, &a.Username, &a.Cash, &a.Fuel, &a.Level,
&a.MaxFuel, &a.MaxPax); err != nil {
return nil, err
}
out = append(out, a)
}
return out, rows.Err()
}
func (c *AdminClient) GiveAirportCash(airportID uint32, amount uint64) error {
_, err := c.db.ExecContext(c.ctx,
`UPDATE airports SET cash = cash + ? WHERE id = ?`, amount, airportID)
return err
}
func (c *AdminClient) GiveAirportBuilding(airportID, buildingID uint32) error {
_, err := c.db.ExecContext(c.ctx,
`INSERT INTO airport_buildings (airport_id, building_id) VALUES (?, ?)`, airportID, buildingID)
return err
}
// ── Update methods ────────────────────────────────────────────────────────────
func (c *AdminClient) UpdateProduct(id uint16, name string) error {
_, err := c.db.ExecContext(c.ctx, `UPDATE products SET name=? WHERE id=?`, name, id)
return err
}
func (c *AdminClient) UpdateCountry(id uint16, name, continent string, p1, p2, p3 uint16) error {
_, err := c.db.ExecContext(c.ctx,
`UPDATE countries SET name=?, continent=?, produce_1_id=?, produce_2_id=?, produce_3_id=? WHERE id=?`,
name, continent, p1, p2, p3, id)
return err
}
func (c *AdminClient) UpdateBuilding(id uint32, p AddBuildingParams) error {
var cap, passRate interface{}
var storType, storSubtype, opType, opSize, passType interface{}
if p.BuildingType == "storage" {
cap = p.Capacity
storType = p.StorageType
storSubtype = p.StorageSubtype
}
if p.BuildingType == "operation" {
opType = p.OperationType
opSize = p.OperationSize
}
if p.BuildingType == "passive" {
passType = p.PassiveType
passRate = p.PassiveRate
}
_, err := c.db.ExecContext(c.ctx, `
UPDATE buildings SET
name=?, min_level=?, construction_cost_cash=?, building_type=?,
capacity=?, storage_type=?, storage_subtype=?,
operation_type=?, operation_size=?,
passive_type=?, passive_rate=?
WHERE id=?`,
p.Name, p.MinLevel, p.Cost, p.BuildingType,
cap, storType, storSubtype,
opType, opSize,
passType, passRate,
id)
return err
}
func (c *AdminClient) UpdatePlane(id uint32, p AddPlaneParams) error {
var maxPax, cashRew, cargoRew interface{}
if p.MaxPassengers > 0 {
maxPax = p.MaxPassengers
}
if p.CashReward > 0 {
cashRew = p.CashReward
}
if p.CargoReward > 0 {
cargoRew = p.CargoReward
}
_, err := c.db.ExecContext(c.ctx, `
UPDATE planes SET
name=?, aircraft_size=?, operation_type=?, aircraft_type=?, min_level=?,
operation_cost_fuel=?, operation_cost_cash=?, max_passengers=?,
travel_time_inSeconds=?, buying_cost_cash=?,
completed_flight_experience=?, completed_flight_cash_reward=?,
completed_flight_cargo_reward=?,
boarding_time_seconds=?, fueling_time_seconds=?
WHERE id=?`,
p.Name, p.Size, p.OperationType, p.AircraftType, p.MinLevel,
p.FuelCost, p.CashCost, maxPax, p.TravelSec, p.BuyCost,
p.XP, cashRew, cargoRew,
p.BoardingTimeSec, p.FuelingTimeSec, id)
return err
}
func (c *AdminClient) UpdateUserPassword(username, newPassword string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
_, err = c.db.ExecContext(c.ctx,
`UPDATE users SET password_hash=? WHERE username=?`, string(hash), username)
return err
}
func (c *AdminClient) UpdateAirport(id uint32, cash uint64, level, maxFuel, maxPax uint32) error {
_, err := c.db.ExecContext(c.ctx, `
UPDATE airports SET cash=?, player_level=?, max_fuel_capacity=?, max_passenger_capacity=?
WHERE id=?`,
cash, level, maxFuel, maxPax, id)
return err
}