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 }