package services import ( "net" "os" "strings" "sync" "go.uber.org/zap" ) // GeoIPService provides IP geolocation using MaxMind GeoLite2 databases. // F025: Géolocalisation connexions. // // If a MaxMind database is not available, falls back to a no-op resolver. // To enable MaxMind, set GEOIP_DB_PATH to the path of a GeoLite2-City.mmdb file. type GeoIPService struct { logger *zap.Logger dbPath string mu sync.RWMutex // In production, this would use github.com/oschwald/maxminddb-golang // For now, we implement a basic lookup that can be extended enabled bool } // NewGeoIPService creates a GeoIP service. // Reads GEOIP_DB_PATH from environment. If not set, geolocation is disabled. func NewGeoIPService(logger *zap.Logger) *GeoIPService { dbPath := os.Getenv("GEOIP_DB_PATH") enabled := false if dbPath != "" { if _, err := os.Stat(dbPath); err == nil { enabled = true logger.Info("GeoIP service enabled", zap.String("db_path", dbPath)) } else { logger.Warn("GeoIP database not found, geolocation disabled", zap.String("db_path", dbPath)) } } else { logger.Info("GeoIP disabled (GEOIP_DB_PATH not set)") } return &GeoIPService{ logger: logger, dbPath: dbPath, enabled: enabled, } } // Lookup resolves an IP address to country (ISO 3166-1 alpha-2) and city. // F025: Returns empty strings if GeoIP is not available or IP is private/localhost. func (s *GeoIPService) Lookup(ip string) (country, city string) { if !s.enabled { return "", "" } // Skip private/localhost IPs parsed := net.ParseIP(ip) if parsed == nil || parsed.IsLoopback() || parsed.IsPrivate() || parsed.IsUnspecified() { return "", "" } // Strip IPv4-mapped IPv6 prefix ipStr := parsed.String() if strings.HasPrefix(ipStr, "::ffff:") { ipStr = strings.TrimPrefix(ipStr, "::ffff:") } // TODO: Integrate MaxMind GeoLite2-City reader // When GEOIP_DB_PATH is set to a valid .mmdb file, use: // db, _ := maxminddb.Open(s.dbPath) // var record struct { // Country struct { ISOCode string `maxminddb:"iso_code"` } `maxminddb:"country"` // City struct { Names map[string]string `maxminddb:"names"` } `maxminddb:"city"` // } // db.Lookup(parsed, &record) // country = record.Country.ISOCode // city = record.City.Names["en"] // // For now, return empty strings (geolocation column populated when MaxMind DB is installed) return "", "" } // IsEnabled returns whether GeoIP resolution is active. func (s *GeoIPService) IsEnabled() bool { s.mu.RLock() defer s.mu.RUnlock() return s.enabled }