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 }