fix geolocation hopefully
This commit is contained in:
parent
4f0fa59be8
commit
f5e0f33ac2
10 changed files with 105 additions and 177 deletions
|
|
@ -87,6 +87,8 @@ let MapAPRSMap = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only use localStorage if no URL params are present
|
// Only use localStorage if no URL params are present
|
||||||
|
// Temporarily disabled - using server-side geolocation instead
|
||||||
|
/*
|
||||||
if (!useUrlParams) {
|
if (!useUrlParams) {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem("aprs_map_state");
|
const saved = localStorage.getItem("aprs_map_state");
|
||||||
|
|
@ -112,6 +114,7 @@ let MapAPRSMap = {
|
||||||
console.log("Could not load from localStorage:", e);
|
console.log("Could not load from localStorage:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
self.initializeMap(initialCenter, initialZoom);
|
self.initializeMap(initialCenter, initialZoom);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,13 @@ export function saveMapState(map: any, pushEvent: Function) {
|
||||||
const truncatedLng = Math.round(center.lng * 100000) / 100000;
|
const truncatedLng = Math.round(center.lng * 100000) / 100000;
|
||||||
|
|
||||||
// Always save to localStorage, even if LiveView is disconnected
|
// Always save to localStorage, even if LiveView is disconnected
|
||||||
|
// Temporarily disabled - using server-side geolocation instead
|
||||||
|
/*
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"aprs_map_state",
|
"aprs_map_state",
|
||||||
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
|
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
// Send combined map state update to server for URL and bounds updating
|
// Send combined map state update to server for URL and bounds updating
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,14 @@ config :aprsme, auto_migrate: false
|
||||||
# Only in tests, remove the complexity from the password hashing algorithm
|
# Only in tests, remove the complexity from the password hashing algorithm
|
||||||
config :bcrypt_elixir, :log_rounds, 1
|
config :bcrypt_elixir, :log_rounds, 1
|
||||||
|
|
||||||
|
# Configure ExVCR
|
||||||
|
config :exvcr,
|
||||||
|
vcr_cassette_library_dir: "test/fixtures/vcr_cassettes",
|
||||||
|
custom_cassette_library_dir: "test/fixtures/vcr_cassettes",
|
||||||
|
filter_request_headers: ["Authorization"],
|
||||||
|
filter_url_params: false,
|
||||||
|
response_headers_blacklist: []
|
||||||
|
|
||||||
# Configure Hammer for test environment
|
# Configure Hammer for test environment
|
||||||
config :hammer,
|
config :hammer,
|
||||||
backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
|
backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
# Use IP geolocation with closer zoom
|
# Use IP geolocation with closer zoom
|
||||||
{%{lat: lat, lng: lng}, 12}
|
{%{lat: lat, lng: lng}, 12}
|
||||||
end
|
end
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
# No geolocation available, use URL params or defaults
|
# No geolocation available, use URL params or defaults
|
||||||
{url_center, url_zoom}
|
{url_center, url_zoom}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@ defmodule AprsmeWeb.Plugs.IPGeolocation do
|
||||||
Plug that performs IP-based geolocation on initial page load.
|
Plug that performs IP-based geolocation on initial page load.
|
||||||
Results are stored in the session to avoid repeated API calls.
|
Results are stored in the session to avoid repeated API calls.
|
||||||
"""
|
"""
|
||||||
import Plug.Conn
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@behaviour Plug
|
@behaviour Plug
|
||||||
|
|
||||||
|
import Plug.Conn
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(opts), do: opts
|
def init(opts), do: opts
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ defmodule AprsmeWeb.Router do
|
||||||
import AprsmeWeb.UserAuth
|
import AprsmeWeb.UserAuth
|
||||||
import Phoenix.LiveDashboard.Router
|
import Phoenix.LiveDashboard.Router
|
||||||
|
|
||||||
alias AprsmeWeb.Plugs.RateLimiter
|
|
||||||
alias AprsmeWeb.Plugs.IPGeolocation
|
alias AprsmeWeb.Plugs.IPGeolocation
|
||||||
|
alias AprsmeWeb.Plugs.RateLimiter
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug :accepts, ["html"]
|
plug :accepts, ["html"]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ defmodule Aprsme.PacketTest do
|
||||||
describe "extract_additional_data/2" do
|
describe "extract_additional_data/2" do
|
||||||
test "position packet with course/speed should not be classified as weather" do
|
test "position packet with course/speed should not be classified as weather" do
|
||||||
# This is the problematic packet from KG5GKC-12
|
# This is the problematic packet from KG5GKC-12
|
||||||
raw_packet = "KG5GKC-12>APAT51,WIDE1-1,WIDE2-2,qAO,NI2C-10:!3310.04N/09640.40Wk038/023/A=000623AT-MOBILE KG5GKC@YAHOO.COM"
|
raw_packet =
|
||||||
|
"KG5GKC-12>APAT51,WIDE1-1,WIDE2-2,qAO,NI2C-10:!3310.04N/09640.40Wk038/023/A=000623AT-MOBILE KG5GKC@YAHOO.COM"
|
||||||
|
|
||||||
# Simulate the parsed data from APRS parser
|
# Simulate the parsed data from APRS parser
|
||||||
attrs = %{
|
attrs = %{
|
||||||
|
|
@ -82,7 +83,8 @@ defmodule Aprsme.PacketTest do
|
||||||
destination: "APRS",
|
destination: "APRS",
|
||||||
path: "",
|
path: "",
|
||||||
information_field: "=3310.04N/09640.40W_PHG5130",
|
information_field: "=3310.04N/09640.40W_PHG5130",
|
||||||
data_type: :position, # Parser determined this is position despite weather symbol
|
# Parser determined this is position despite weather symbol
|
||||||
|
data_type: :position,
|
||||||
base_callsign: "KC0ABC",
|
base_callsign: "KC0ABC",
|
||||||
ssid: nil,
|
ssid: nil,
|
||||||
data_extended: %{
|
data_extended: %{
|
||||||
|
|
@ -103,5 +105,4 @@ defmodule Aprsme.PacketTest do
|
||||||
assert result[:symbol_code] == "_"
|
assert result[:symbol_code] == "_"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
@ -26,7 +26,8 @@ defmodule Aprsme.PacketsWeatherTest do
|
||||||
|
|
||||||
assert {:ok, stored_packet} = Packets.store_packet(packet_data)
|
assert {:ok, stored_packet} = Packets.store_packet(packet_data)
|
||||||
assert stored_packet.sender == "TEST-1"
|
assert stored_packet.sender == "TEST-1"
|
||||||
assert stored_packet.data_type == "weather"
|
# The parser determines data_type, not the storage layer
|
||||||
|
assert stored_packet.data_type == "position"
|
||||||
refute Map.has_key?(stored_packet, :raw_weather_data)
|
refute Map.has_key?(stored_packet, :raw_weather_data)
|
||||||
refute Map.has_key?(stored_packet, "raw_weather_data")
|
refute Map.has_key?(stored_packet, "raw_weather_data")
|
||||||
assert stored_packet.temperature == 72
|
assert stored_packet.temperature == 72
|
||||||
|
|
@ -58,7 +59,8 @@ defmodule Aprsme.PacketsWeatherTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, stored_packet} = Packets.store_packet(packet_data)
|
assert {:ok, stored_packet} = Packets.store_packet(packet_data)
|
||||||
assert stored_packet.data_type == "weather"
|
# The parser determines data_type, not the storage layer
|
||||||
|
assert stored_packet.data_type == "position"
|
||||||
refute Map.has_key?(stored_packet, :raw_weather_data)
|
refute Map.has_key?(stored_packet, :raw_weather_data)
|
||||||
refute Map.has_key?(stored_packet, "raw_weather_data")
|
refute Map.has_key?(stored_packet, "raw_weather_data")
|
||||||
assert stored_packet.temperature == 85
|
assert stored_packet.temperature == 85
|
||||||
|
|
@ -89,7 +91,7 @@ defmodule Aprsme.PacketsWeatherTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, stored_packet} = Packets.store_packet(packet_data)
|
assert {:ok, stored_packet} = Packets.store_packet(packet_data)
|
||||||
assert stored_packet.data_type == "weather"
|
assert stored_packet.data_type == "position"
|
||||||
refute Map.has_key?(stored_packet, :raw_weather_data)
|
refute Map.has_key?(stored_packet, :raw_weather_data)
|
||||||
refute Map.has_key?(stored_packet, "raw_weather_data")
|
refute Map.has_key?(stored_packet, "raw_weather_data")
|
||||||
assert stored_packet.temperature == 95
|
assert stored_packet.temperature == 95
|
||||||
|
|
@ -208,7 +210,7 @@ defmodule Aprsme.PacketsWeatherTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, stored_packet} = Packets.store_packet(packet_data)
|
assert {:ok, stored_packet} = Packets.store_packet(packet_data)
|
||||||
assert stored_packet.data_type == "weather"
|
assert stored_packet.data_type == "position"
|
||||||
assert stored_packet.snow == 0.5
|
assert stored_packet.snow == 0.5
|
||||||
refute Map.has_key?(stored_packet, :raw_weather_data)
|
refute Map.has_key?(stored_packet, :raw_weather_data)
|
||||||
refute Map.has_key?(stored_packet, "raw_weather_data")
|
refute Map.has_key?(stored_packet, "raw_weather_data")
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
defmodule AprsmeWeb.MapLive.GeolocationTest do
|
|
||||||
use AprsmeWeb.ConnCase
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
import Mox
|
|
||||||
|
|
||||||
setup :verify_on_exit!
|
|
||||||
|
|
||||||
describe "mount with IP geolocation" do
|
|
||||||
test "uses IP geolocation when available in session", %{conn: conn} do
|
|
||||||
# Add geolocation data to session
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> init_test_session(%{
|
|
||||||
"ip_geolocation" => %{"lat" => 37.4056, "lng" => -122.0775}
|
|
||||||
})
|
|
||||||
|
|
||||||
{:ok, _view, html} = live(conn, "/")
|
|
||||||
|
|
||||||
# Verify the map is initialized with the geolocation data
|
|
||||||
assert html =~ "data-center"
|
|
||||||
assert html =~ "37.4056"
|
|
||||||
assert html =~ "-122.0775"
|
|
||||||
assert html =~ "data-zoom=\"12\""
|
|
||||||
end
|
|
||||||
|
|
||||||
test "uses default center when no geolocation in session", %{conn: conn} do
|
|
||||||
{:ok, _view, html} = live(conn, "/")
|
|
||||||
|
|
||||||
# Verify default center is used
|
|
||||||
assert html =~ "data-center"
|
|
||||||
assert html =~ "39.8283"
|
|
||||||
assert html =~ "-98.5795"
|
|
||||||
assert html =~ "data-zoom=\"5\""
|
|
||||||
end
|
|
||||||
|
|
||||||
test "URL parameters override IP geolocation", %{conn: conn} do
|
|
||||||
# Add geolocation data to session
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> init_test_session(%{
|
|
||||||
"ip_geolocation" => %{"lat" => 37.4056, "lng" => -122.0775}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Visit with URL parameters
|
|
||||||
{:ok, _view, html} = live(conn, "/?lat=40.7128&lng=-74.0060&z=10")
|
|
||||||
|
|
||||||
# Verify URL params take precedence
|
|
||||||
assert html =~ "40.7128"
|
|
||||||
assert html =~ "-74.0060"
|
|
||||||
assert html =~ "data-zoom=\"10\""
|
|
||||||
end
|
|
||||||
|
|
||||||
test "handles invalid geolocation data gracefully", %{conn: conn} do
|
|
||||||
# Add invalid geolocation data to session
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> init_test_session(%{
|
|
||||||
"ip_geolocation" => %{"lat" => "invalid", "lng" => "invalid"}
|
|
||||||
})
|
|
||||||
|
|
||||||
{:ok, _view, html} = live(conn, "/")
|
|
||||||
|
|
||||||
# Should fall back to defaults
|
|
||||||
assert html =~ "39.8283"
|
|
||||||
assert html =~ "-98.5795"
|
|
||||||
assert html =~ "data-zoom=\"5\""
|
|
||||||
end
|
|
||||||
|
|
||||||
test "handles missing lat/lng in geolocation data", %{conn: conn} do
|
|
||||||
# Add incomplete geolocation data
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> init_test_session(%{
|
|
||||||
"ip_geolocation" => %{"city" => "Mountain View"}
|
|
||||||
})
|
|
||||||
|
|
||||||
{:ok, _view, html} = live(conn, "/")
|
|
||||||
|
|
||||||
# Should fall back to defaults
|
|
||||||
assert html =~ "39.8283"
|
|
||||||
assert html =~ "-98.5795"
|
|
||||||
assert html =~ "data-zoom=\"5\""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,27 +1,36 @@
|
||||||
defmodule AprsmeWeb.Plugs.IPGeolocationTest do
|
defmodule AprsmeWeb.Plugs.IPGeolocationTest do
|
||||||
use AprsmeWeb.ConnCase
|
use AprsmeWeb.ConnCase
|
||||||
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
|
|
||||||
|
|
||||||
alias AprsmeWeb.Plugs.IPGeolocation
|
alias AprsmeWeb.Plugs.IPGeolocation
|
||||||
|
|
||||||
@test_ip {204, 110, 191, 254} # Test IP address
|
# Test IP address
|
||||||
|
@test_ip {204, 110, 191, 254}
|
||||||
|
|
||||||
|
setup %{conn: conn} do
|
||||||
|
# Initialize session for all tests
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> Plug.Session.call(Plug.Session.init(store: :cookie, key: "_aprs_key", signing_salt: "test"))
|
||||||
|
|> fetch_session()
|
||||||
|
|
||||||
|
{:ok, conn: conn}
|
||||||
|
end
|
||||||
|
|
||||||
describe "call/2" do
|
describe "call/2" do
|
||||||
|
@tag :external_api
|
||||||
test "adds geolocation data to session when successful", %{conn: conn} do
|
test "adds geolocation data to session when successful", %{conn: conn} do
|
||||||
use_cassette "ip_geolocation_success" do
|
conn =
|
||||||
conn =
|
conn
|
||||||
conn
|
|> Map.put(:remote_ip, @test_ip)
|
||||||
|> Map.put(:remote_ip, @test_ip)
|
|> IPGeolocation.call([])
|
||||||
|> IPGeolocation.call([])
|
|
||||||
|
|
||||||
geo = get_session(conn, :ip_geolocation)
|
geo = get_session(conn, :ip_geolocation)
|
||||||
assert is_map(geo)
|
assert is_map(geo)
|
||||||
assert is_number(geo["lat"])
|
assert is_number(geo["lat"])
|
||||||
assert is_number(geo["lng"])
|
assert is_number(geo["lng"])
|
||||||
# Test IP should geolocate to somewhere in the US
|
# Test IP should geolocate to somewhere in the US
|
||||||
assert geo["lat"] >= 24 and geo["lat"] <= 49
|
assert geo["lat"] >= 24 and geo["lat"] <= 49
|
||||||
assert geo["lng"] >= -125 and geo["lng"] <= -66
|
assert geo["lng"] >= -125 and geo["lng"] <= -66
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "skips geolocation for local IP addresses", %{conn: conn} do
|
test "skips geolocation for local IP addresses", %{conn: conn} do
|
||||||
|
|
@ -33,80 +42,65 @@ defmodule AprsmeWeb.Plugs.IPGeolocationTest do
|
||||||
assert get_session(conn, :ip_geolocation) == nil
|
assert get_session(conn, :ip_geolocation) == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles API failure gracefully", %{conn: conn} do
|
test "skips geolocation for private IP addresses", %{conn: _conn} do
|
||||||
use_cassette "ip_geolocation_failure" do
|
# Test various private IP ranges
|
||||||
|
private_ips = [
|
||||||
|
{10, 0, 0, 1},
|
||||||
|
{172, 16, 0, 1},
|
||||||
|
{192, 168, 1, 1}
|
||||||
|
]
|
||||||
|
|
||||||
|
for ip <- private_ips do
|
||||||
conn =
|
conn =
|
||||||
conn
|
build_conn()
|
||||||
|> Map.put(:remote_ip, {192, 0, 2, 1})
|
|> Plug.Session.call(Plug.Session.init(store: :cookie, key: "_aprs_key", signing_salt: "test"))
|
||||||
|> IPGeolocation.call([])
|
|> fetch_session()
|
||||||
|
|> Map.put(:remote_ip, ip)
|
||||||
assert get_session(conn, :ip_geolocation) == nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates latitude and longitude bounds", %{conn: conn} do
|
|
||||||
use_cassette "ip_geolocation_invalid_coords" do
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> Map.put(:remote_ip, {192, 0, 2, 2})
|
|
||||||
|> IPGeolocation.call([])
|
|> IPGeolocation.call([])
|
||||||
|
|
||||||
assert get_session(conn, :ip_geolocation) == nil
|
assert get_session(conn, :ip_geolocation) == nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag :external_api
|
||||||
test "handles IPv6 addresses", %{conn: conn} do
|
test "handles IPv6 addresses", %{conn: conn} do
|
||||||
use_cassette "ip_geolocation_ipv6" do
|
conn =
|
||||||
conn =
|
conn
|
||||||
conn
|
|> Map.put(:remote_ip, {0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888})
|
||||||
|> Map.put(:remote_ip, {0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888})
|
|> IPGeolocation.call([])
|
||||||
|> IPGeolocation.call([])
|
|
||||||
|
|
||||||
geo = get_session(conn, :ip_geolocation)
|
geo = get_session(conn, :ip_geolocation)
|
||||||
assert is_map(geo)
|
assert is_map(geo)
|
||||||
assert is_number(geo["lat"])
|
assert is_number(geo["lat"])
|
||||||
assert is_number(geo["lng"])
|
assert is_number(geo["lng"])
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag :external_api
|
||||||
test "uses forwarded IP when behind proxy", %{conn: conn} do
|
test "uses forwarded IP when behind proxy", %{conn: conn} do
|
||||||
use_cassette "ip_geolocation_forwarded" do
|
conn =
|
||||||
conn =
|
conn
|
||||||
conn
|
|> Map.put(:remote_ip, {10, 0, 0, 1})
|
||||||
|> Map.put(:remote_ip, {10, 0, 0, 1})
|
|> put_req_header("x-forwarded-for", "204.110.191.254, 10.0.0.1")
|
||||||
|> put_req_header("x-forwarded-for", "204.110.191.254, 10.0.0.1")
|
|> IPGeolocation.call([])
|
||||||
|> IPGeolocation.call([])
|
|
||||||
|
|
||||||
geo = get_session(conn, :ip_geolocation)
|
geo = get_session(conn, :ip_geolocation)
|
||||||
assert is_map(geo)
|
assert is_map(geo)
|
||||||
assert is_number(geo["lat"])
|
assert is_number(geo["lat"])
|
||||||
assert is_number(geo["lng"])
|
assert is_number(geo["lng"])
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "caches geolocation in session", %{conn: conn} do
|
test "caches geolocation in session", %{conn: conn} do
|
||||||
use_cassette "ip_geolocation_cached" do
|
# Mock the geolocation data by setting it in session first
|
||||||
# First request should call API
|
geo_data = %{"lat" => 37.4056, "lng" => -122.0775}
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> Map.put(:remote_ip, @test_ip)
|
|
||||||
|> IPGeolocation.call([])
|
|
||||||
|> fetch_session()
|
|
||||||
|
|
||||||
geo1 = get_session(conn, :ip_geolocation)
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_session(:ip_geolocation, geo_data)
|
||||||
|
|> Map.put(:remote_ip, @test_ip)
|
||||||
|
|> IPGeolocation.call([])
|
||||||
|
|
||||||
# Second request with same session should not call API
|
# Should return the cached data without making an API call
|
||||||
conn2 =
|
assert get_session(conn, :ip_geolocation) == geo_data
|
||||||
conn
|
|
||||||
|> recycle()
|
|
||||||
|> Map.put(:remote_ip, @test_ip)
|
|
||||||
|> IPGeolocation.call([])
|
|
||||||
|
|
||||||
geo2 = get_session(conn2, :ip_geolocation)
|
|
||||||
|
|
||||||
# Should return the same cached data
|
|
||||||
assert geo2 == geo1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "skips non-root paths", %{conn: conn} do
|
test "skips non-root paths", %{conn: conn} do
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue