eq2go/internal/login/database.go

350 lines
9.5 KiB
Go

package login
import (
"fmt"
"strings"
"time"
"eq2emu/internal/database"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// LoginAccount represents a login account
type LoginAccount struct {
ID int32 `json:"id"`
Username string `json:"username"`
Password string `json:"password"` // MD5 hash
Email string `json:"email"`
Status string `json:"status"` // Active, Suspended, Banned
AccessLevel int16 `json:"access_level"`
CreatedDate int64 `json:"created_date"`
LastLogin int64 `json:"last_login"`
LastIP string `json:"last_ip"`
}
// Character represents a character
type Character struct {
ID int32 `json:"id"`
AccountID int32 `json:"account_id"`
Name string `json:"name"`
Race int8 `json:"race"`
Class int8 `json:"class"`
Gender int8 `json:"gender"`
Level int16 `json:"level"`
Zone int32 `json:"zone"`
ZoneInstance int32 `json:"zone_instance"`
ServerID int16 `json:"server_id"`
LastPlayed int64 `json:"last_played"`
CreatedDate int64 `json:"created_date"`
DeletedDate int64 `json:"deleted_date"`
}
// LoginDB wraps the base Database with login-specific methods
type LoginDB struct {
*database.Database
}
// NewLoginDB creates a new database connection for login server
func NewLoginDB(dbType, dsn string) (*LoginDB, error) {
var db *database.Database
var err error
switch strings.ToLower(dbType) {
case "sqlite":
db, err = database.NewSQLite(dsn)
case "mysql":
db, err = database.NewMySQL(dsn)
default:
return nil, fmt.Errorf("unsupported database type: %s", dbType)
}
if err != nil {
return nil, err
}
loginDB := &LoginDB{Database: db}
return loginDB, nil
}
// GetLoginAccount retrieves a login account by username and password
func (db *LoginDB) GetLoginAccount(username, hashedPassword string) (*LoginAccount, error) {
var account LoginAccount
query := "SELECT id, username, password, email, status, access_level, created_date, last_login, last_ip FROM login_accounts WHERE username = ? AND password = ?"
if db.GetType() == database.SQLite {
found := false
err := db.ExecTransient(query,
func(stmt *sqlite.Stmt) error {
account.ID = int32(stmt.ColumnInt64(0))
account.Username = stmt.ColumnText(1)
account.Password = stmt.ColumnText(2)
account.Email = stmt.ColumnText(3)
account.Status = stmt.ColumnText(4)
account.AccessLevel = int16(stmt.ColumnInt64(5))
account.CreatedDate = stmt.ColumnInt64(6)
account.LastLogin = stmt.ColumnInt64(7)
account.LastIP = stmt.ColumnText(8)
found = true
return nil
},
username, hashedPassword,
)
if err != nil {
return nil, fmt.Errorf("database query error: %w", err)
}
if !found {
return nil, fmt.Errorf("account not found")
}
} else {
// MySQL implementation
row := db.QueryRow(query, username, hashedPassword)
err := row.Scan(
&account.ID,
&account.Username,
&account.Password,
&account.Email,
&account.Status,
&account.AccessLevel,
&account.CreatedDate,
&account.LastLogin,
&account.LastIP,
)
if err != nil {
return nil, fmt.Errorf("account not found or database error: %w", err)
}
}
return &account, nil
}
// GetCharacters retrieves all characters for an account
func (db *LoginDB) GetCharacters(accountID int32) ([]*Character, error) {
var characters []*Character
err := db.ExecTransient(
`SELECT id, account_id, name, race, class, gender, level, zone_id, zone_instance,
server_id, last_played, created_date, deleted_date
FROM characters
WHERE account_id = ? AND deleted_date = 0
ORDER BY last_played DESC`,
func(stmt *sqlite.Stmt) error {
char := &Character{
ID: int32(stmt.ColumnInt64(0)),
AccountID: int32(stmt.ColumnInt64(1)),
Name: stmt.ColumnText(2),
Race: int8(stmt.ColumnInt64(3)),
Class: int8(stmt.ColumnInt64(4)),
Gender: int8(stmt.ColumnInt64(5)),
Level: int16(stmt.ColumnInt64(6)),
Zone: int32(stmt.ColumnInt64(7)),
ZoneInstance: int32(stmt.ColumnInt64(8)),
ServerID: int16(stmt.ColumnInt64(9)),
LastPlayed: stmt.ColumnInt64(10),
CreatedDate: stmt.ColumnInt64(11),
DeletedDate: stmt.ColumnInt64(12),
}
characters = append(characters, char)
return nil
},
accountID,
)
if err != nil {
return nil, fmt.Errorf("failed to load characters: %w", err)
}
return characters, nil
}
// UpdateLastLogin updates the last login time and IP for an account
func (db *LoginDB) UpdateLastLogin(accountID int32, ipAddress string) error {
now := time.Now().Unix()
query := "UPDATE login_accounts SET last_login = ?, last_ip = ? WHERE id = ?"
if db.GetType() == database.SQLite {
return db.Execute(query, &sqlitex.ExecOptions{
Args: []any{now, ipAddress, accountID},
})
} else {
// MySQL implementation
_, err := db.Exec(query, now, ipAddress, accountID)
return err
}
}
// UpdateServerStats updates server statistics
func (db *LoginDB) UpdateServerStats(serverType string, clientCount, worldCount int) error {
now := time.Now().Unix()
if db.GetType() == database.SQLite {
return db.Execute(
`INSERT OR REPLACE INTO server_stats (server_type, client_count, world_count, last_update)
VALUES (?, ?, ?, ?)`,
&sqlitex.ExecOptions{
Args: []any{serverType, clientCount, worldCount, now},
},
)
} else {
// MySQL implementation using ON DUPLICATE KEY UPDATE
query := `INSERT INTO server_stats (server_type, client_count, world_count, last_update)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
client_count = VALUES(client_count),
world_count = VALUES(world_count),
last_update = VALUES(last_update)`
_, err := db.Exec(query, serverType, clientCount, worldCount, now)
return err
}
}
// CreateAccount creates a new login account
func (db *LoginDB) CreateAccount(username, hashedPassword, email string, accessLevel int16) (*LoginAccount, error) {
now := time.Now().Unix()
// Check if username already exists
exists := false
err := db.ExecTransient(
"SELECT 1 FROM login_accounts WHERE username = ?",
func(stmt *sqlite.Stmt) error {
exists = true
return nil
},
username,
)
if err != nil {
return nil, fmt.Errorf("failed to check username: %w", err)
}
if exists {
return nil, fmt.Errorf("username already exists")
}
// Insert new account
var accountID int32
err = db.Execute(
`INSERT INTO login_accounts (username, password, email, access_level, created_date, status)
VALUES (?, ?, ?, ?, ?, 'Active')`,
&sqlitex.ExecOptions{
Args: []any{username, hashedPassword, email, accessLevel, now},
ResultFunc: func(stmt *sqlite.Stmt) error {
accountID = int32(stmt.ColumnInt64(0))
return nil
},
},
)
if err != nil {
return nil, fmt.Errorf("failed to create account: %w", err)
}
// Return the created account
return &LoginAccount{
ID: accountID,
Username: username,
Password: hashedPassword,
Email: email,
Status: "Active",
AccessLevel: accessLevel,
CreatedDate: now,
LastLogin: 0,
LastIP: "",
}, nil
}
// GetCharacterByID retrieves a character by ID
func (db *LoginDB) GetCharacterByID(characterID int32) (*Character, error) {
var character Character
found := false
err := db.ExecTransient(
`SELECT id, account_id, name, race, class, gender, level, zone_id, zone_instance,
server_id, last_played, created_date, deleted_date
FROM characters WHERE id = ?`,
func(stmt *sqlite.Stmt) error {
character.ID = int32(stmt.ColumnInt64(0))
character.AccountID = int32(stmt.ColumnInt64(1))
character.Name = stmt.ColumnText(2)
character.Race = int8(stmt.ColumnInt64(3))
character.Class = int8(stmt.ColumnInt64(4))
character.Gender = int8(stmt.ColumnInt64(5))
character.Level = int16(stmt.ColumnInt64(6))
character.Zone = int32(stmt.ColumnInt64(7))
character.ZoneInstance = int32(stmt.ColumnInt64(8))
character.ServerID = int16(stmt.ColumnInt64(9))
character.LastPlayed = stmt.ColumnInt64(10)
character.CreatedDate = stmt.ColumnInt64(11)
character.DeletedDate = stmt.ColumnInt64(12)
found = true
return nil
},
characterID,
)
if err != nil {
return nil, fmt.Errorf("database query error: %w", err)
}
if !found {
return nil, fmt.Errorf("character not found")
}
return &character, nil
}
// DeleteCharacter marks a character as deleted
func (db *LoginDB) DeleteCharacter(characterID int32) error {
now := time.Now().Unix()
return db.Execute(
"UPDATE characters SET deleted_date = ? WHERE id = ?",
&sqlitex.ExecOptions{
Args: []any{now, characterID},
},
)
}
// GetAccountStats retrieves statistics about login accounts
func (db *LoginDB) GetAccountStats() (map[string]int, error) {
stats := make(map[string]int)
// Count total accounts
err := db.ExecTransient(
"SELECT COUNT(*) FROM login_accounts",
func(stmt *sqlite.Stmt) error {
stats["total_accounts"] = int(stmt.ColumnInt64(0))
return nil
},
)
if err != nil {
return nil, err
}
// Count active accounts
err = db.ExecTransient(
"SELECT COUNT(*) FROM login_accounts WHERE status = 'Active'",
func(stmt *sqlite.Stmt) error {
stats["active_accounts"] = int(stmt.ColumnInt64(0))
return nil
},
)
if err != nil {
return nil, err
}
// Count total characters
err = db.ExecTransient(
"SELECT COUNT(*) FROM characters WHERE deleted_date = 0",
func(stmt *sqlite.Stmt) error {
stats["total_characters"] = int(stmt.ColumnInt64(0))
return nil
},
)
if err != nil {
return nil, err
}
return stats, nil
}