mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-07-06 09:52:02 +02:00
Add authenticated POST endpoint for node updates (#11)
This commit is contained in:
+1
-2
@@ -65,8 +65,7 @@ def upsert_node(node_id, n):
|
||||
_get(pos, "latitude"),
|
||||
_get(pos, "longitude"),
|
||||
_get(pos, "altitude"),
|
||||
# @TODO json.dumps(_jsonable(n), ensure_ascii=False),
|
||||
"{'foo'}",
|
||||
json.dumps(_jsonable(n), ensure_ascii=False),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
|
||||
@@ -41,3 +41,39 @@ def test_query_nodes_from_web_app(tmp_path):
|
||||
assert len(data) == 5
|
||||
last_heards = [item["last_heard"] for item in data]
|
||||
assert last_heards == sorted(last_heards, reverse=True)
|
||||
|
||||
|
||||
def test_post_nodes_to_web_app(tmp_path):
|
||||
db_path = tmp_path / "nodes.db"
|
||||
os.environ["MESH_DB"] = str(db_path)
|
||||
os.environ["API_TOKEN"] = "secrettoken"
|
||||
|
||||
web_dir = Path(__file__).resolve().parents[1] / "web"
|
||||
nodes_json = Path(__file__).with_name("nodes.json")
|
||||
try:
|
||||
subprocess.run(["bundle", "install"], cwd=web_dir, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
env = os.environ.copy()
|
||||
env["MESH_DB"] = str(db_path)
|
||||
env["API_TOKEN"] = "secrettoken"
|
||||
ruby = (
|
||||
"require_relative 'app'; require 'json'; require 'rack/mock'; require 'sqlite3';"\
|
||||
f"nodes = File.read({json.dumps(str(nodes_json))});"\
|
||||
"req = Rack::MockRequest.new(Sinatra::Application);"\
|
||||
"res = req.post('/api/nodes', 'CONTENT_TYPE' => 'application/json', 'HTTP_AUTHORIZATION' => 'Bearer secrettoken', :input => nodes);"\
|
||||
"puts res.status;"\
|
||||
"db = SQLite3::Database.new(ENV['MESH_DB']);"\
|
||||
"puts db.get_first_value('SELECT COUNT(*) FROM nodes');"
|
||||
)
|
||||
out = subprocess.check_output(
|
||||
["bundle", "exec", "ruby", "-e", ruby],
|
||||
cwd=web_dir,
|
||||
env=env,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
pytest.skip("ruby dependencies not installed")
|
||||
|
||||
lines = out.decode().strip().splitlines()
|
||||
assert lines[0] == "200"
|
||||
expected = len(json.load(open(nodes_json)))
|
||||
assert int(lines[1]) == expected
|
||||
|
||||
+71
@@ -34,6 +34,77 @@ get "/api/nodes" do
|
||||
query_nodes(limit).to_json
|
||||
end
|
||||
|
||||
def upsert_node(db, node_id, n)
|
||||
user = n["user"] || {}
|
||||
met = n["deviceMetrics"] || {}
|
||||
pos = n["position"] || {}
|
||||
row = [
|
||||
node_id,
|
||||
n["num"],
|
||||
user["shortName"],
|
||||
user["longName"],
|
||||
user["macaddr"],
|
||||
user["hwModel"] || n["hwModel"],
|
||||
user["role"],
|
||||
user["publicKey"],
|
||||
user["isUnmessagable"],
|
||||
n["isFavorite"],
|
||||
n["hopsAway"],
|
||||
n["snr"],
|
||||
n["lastHeard"],
|
||||
met["batteryLevel"],
|
||||
met["voltage"],
|
||||
met["channelUtilization"],
|
||||
met["airUtilTx"],
|
||||
met["uptimeSeconds"],
|
||||
pos["time"],
|
||||
pos["locationSource"],
|
||||
pos["latitude"],
|
||||
pos["longitude"],
|
||||
pos["altitude"],
|
||||
JSON.dump(n)
|
||||
]
|
||||
db.execute <<~SQL, row
|
||||
INSERT INTO nodes(node_id,num,short_name,long_name,macaddr,hw_model,role,public_key,is_unmessagable,is_favorite,
|
||||
hops_away,snr,last_heard,battery_level,voltage,channel_utilization,air_util_tx,uptime_seconds,
|
||||
position_time,location_source,latitude,longitude,altitude,node_json)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT(node_id) DO UPDATE SET
|
||||
num=excluded.num, short_name=excluded.short_name, long_name=excluded.long_name, macaddr=excluded.macaddr,
|
||||
hw_model=excluded.hw_model, role=excluded.role, public_key=excluded.public_key, is_unmessagable=excluded.is_unmessagable,
|
||||
is_favorite=excluded.is_favorite, hops_away=excluded.hops_away, snr=excluded.snr, last_heard=excluded.last_heard,
|
||||
battery_level=excluded.battery_level, voltage=excluded.voltage, channel_utilization=excluded.channel_utilization,
|
||||
air_util_tx=excluded.air_util_tx, uptime_seconds=excluded.uptime_seconds, position_time=excluded.position_time,
|
||||
location_source=excluded.location_source, latitude=excluded.latitude, longitude=excluded.longitude,
|
||||
altitude=excluded.altitude, node_json=excluded.node_json
|
||||
WHERE COALESCE(excluded.last_heard,0) >= COALESCE(nodes.last_heard,0)
|
||||
SQL
|
||||
end
|
||||
|
||||
def require_token!
|
||||
token = ENV["API_TOKEN"]
|
||||
provided = request.env["HTTP_AUTHORIZATION"].to_s.sub(/^Bearer\s+/i, "")
|
||||
halt 403, { error: "Forbidden" }.to_json unless token && provided == token
|
||||
end
|
||||
|
||||
post "/api/nodes" do
|
||||
require_token!
|
||||
content_type :json
|
||||
begin
|
||||
data = JSON.parse(request.body.read)
|
||||
rescue JSON::ParserError
|
||||
halt 400, { error: "invalid JSON" }.to_json
|
||||
end
|
||||
halt 400, { error: "too many nodes" }.to_json if data.is_a?(Hash) && data.size > 1000
|
||||
db = SQLite3::Database.new(DB_PATH)
|
||||
data.each do |node_id, node|
|
||||
upsert_node(db, node_id, node)
|
||||
end
|
||||
{ status: "ok" }.to_json
|
||||
ensure
|
||||
db&.close
|
||||
end
|
||||
|
||||
get "/" do
|
||||
send_file File.join(settings.public_folder, "index.html")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user