phase 0.3
This commit is contained in:
commit
4a94bfc4e7
52 changed files with 11653 additions and 0 deletions
548
internal/db/admin_client.go
Normal file
548
internal/db/admin_client.go
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue