548 lines
17 KiB
Go
548 lines
17 KiB
Go
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
|
|
}
|