goonserver/db/db.go

189 lines
3.9 KiB
Go

package db
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
var (
ErrUserExists = errors.New("username already exists")
ErrInvalidCredentials = errors.New("invalid username or password")
)
const (
maxRegistrationsPerIP = 3 // Maximum registrations allowed per IP
registrationWindow = 24 * time.Hour // Time window for rate limiting
)
type registrationAttempt struct {
count int
firstTry time.Time
}
var (
registrationAttempts = make(map[string]*registrationAttempt)
rateLimitMutex sync.RWMutex
)
func CleanupOldAttempts() {
rateLimitMutex.Lock()
defer rateLimitMutex.Unlock()
now := time.Now()
for ip, attempt := range registrationAttempts {
if now.Sub(attempt.firstTry) > registrationWindow {
delete(registrationAttempts, ip)
}
}
}
func CheckRegistrationLimit(ip string) error {
rateLimitMutex.Lock()
defer rateLimitMutex.Unlock()
now := time.Now()
attempt, exists := registrationAttempts[ip]
if !exists {
registrationAttempts[ip] = &registrationAttempt{
count: 1,
firstTry: now,
}
return nil
}
// Reset if window has passed
if now.Sub(attempt.firstTry) > registrationWindow {
attempt.count = 1
attempt.firstTry = now
return nil
}
if attempt.count >= maxRegistrationsPerIP {
return fmt.Errorf("registration limit reached for this IP. Please try again in %v",
registrationWindow-now.Sub(attempt.firstTry))
}
attempt.count++
return nil
}
func InitDB(dbPath string) error {
var err error
db, err = sql.Open("sqlite3", dbPath)
if err != nil {
return err
}
// Create tables if they don't exist
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS players (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at DATETIME NOT NULL
);
CREATE TABLE IF NOT EXISTS player_states (
player_id INTEGER PRIMARY KEY,
x INTEGER NOT NULL,
y INTEGER NOT NULL,
last_seen DATETIME NOT NULL,
FOREIGN KEY(player_id) REFERENCES players(id)
);
`)
return err
}
func hashPassword(password string) string {
hash := sha256.Sum256([]byte(password))
return hex.EncodeToString(hash[:])
}
func RegisterPlayer(username, password string) (int, error) {
// Check if username exists
var exists bool
err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM players WHERE username = ?)", username).Scan(&exists)
if err != nil {
return 0, err
}
if exists {
return 0, ErrUserExists
}
// Create new player
result, err := db.Exec(`
INSERT INTO players (username, password_hash, created_at)
VALUES (?, ?, ?)`,
username, hashPassword(password), time.Now().UTC(),
)
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
return int(id), err
}
func AuthenticatePlayer(username, password string) (int, error) {
var id int
var storedHash string
err := db.QueryRow(`
SELECT id, password_hash
FROM players
WHERE username = ?`,
username,
).Scan(&id, &storedHash)
if err == sql.ErrNoRows {
return 0, ErrInvalidCredentials
}
if err != nil {
return 0, err
}
if storedHash != hashPassword(password) {
return 0, ErrInvalidCredentials
}
return id, nil
}
func SavePlayerState(playerID int, x, y int) error {
_, err := db.Exec(`
INSERT OR REPLACE INTO player_states (
player_id, x, y, last_seen
) VALUES (?, ?, ?, ?)`,
playerID, x, y, time.Now().UTC(),
)
return err
}
func LoadPlayerState(playerID int) (x, y int, err error) {
err = db.QueryRow(`
SELECT x, y FROM player_states
WHERE player_id = ?`,
playerID,
).Scan(&x, &y)
if err == sql.ErrNoRows {
// Return default position for new players
return 5, 5, nil
}
return x, y, err
}
func GetUsername(playerID int) (string, error) {
var username string
err := db.QueryRow("SELECT username FROM players WHERE id = ?", playerID).Scan(&username)
return username, err
}