mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-08-06 02:21:42 +00:00
Merge branch 'main' into 95-send-spots-to-xota
# Conflicts: # README.md # config-example.yml # core/config.py # server/handlers/api/addspot.py # server/handlers/api/options.py # static/js/add-spot.js # static/js/bands.js # static/js/map.js
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
// Credentials schema per provider name. Defines the fields to collect and how to label them.
|
||||
const PROVIDER_CREDENTIAL_SCHEMAS = {
|
||||
// todo Figure out SOTA authentication
|
||||
// see e.g. https://github.com/ham2k/app-polo/blob/main/src/extensions/activities/sota/SOTAAccount.jsx
|
||||
// https://github.com/ham2k/app-polo/blob/main/src/store/apis/apiSOTA/apiSOTA.js
|
||||
// Refresh token? Way to show user that they need to log in again because cached credentials aren't valid?
|
||||
// todo type: text/password distinction on text boxes so API keys can be obscured
|
||||
"SOTA": [
|
||||
{key: "access_token", label: "SOTA Access Token", help: ""},
|
||||
{key: "id_token", label: "SOTA ID Token", help: "TODO SOTA authentication to provide this..."}
|
||||
],
|
||||
"ParksNPeaks": [
|
||||
{key: "user_id", label: "Parks N Peaks User ID", help: ""},
|
||||
{key: "api_key", label: "Parks N Peaks API Key", help: "Get your API key from your Parks N Peaks account."}
|
||||
],
|
||||
"ZLOTA": [
|
||||
{key: "user_id", label: "ZLOTA User ID", help: ""},
|
||||
{key: "api_key", label: "ZLOTA User PIN", help: "Get your PIN from your ZLOTA account."}
|
||||
],
|
||||
"Tiles": [
|
||||
{
|
||||
key: "offline_spot_gateway_pin",
|
||||
label: "Offline Spot Gateway PIN",
|
||||
help: "Get your PIN from your Tiles on the Air account profile."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Load server options. Once a successful callback is made from this, we can populate the choice boxes in the form and load
|
||||
// any saved values from local storage.
|
||||
function loadOptions() {
|
||||
$.getJSON('/api/v2/options', function (jsonData) {
|
||||
// Store options
|
||||
options = jsonData;
|
||||
|
||||
// Populate modes drop-down
|
||||
$.each(options["modes"], function (i, m) {
|
||||
$('#mode').append($('<option>', {
|
||||
value: m,
|
||||
text: m
|
||||
}));
|
||||
});
|
||||
|
||||
// Populate SIG drop-down
|
||||
$.each(options["sigs"], function (i, sig) {
|
||||
$('#sig').append($('<option>', {
|
||||
value: sig.name,
|
||||
text: sig.name
|
||||
}));
|
||||
});
|
||||
|
||||
// Load reCAPTCHA if a site key is configured (key is inlined into page by server)
|
||||
if (window._recaptchaSiteKey) {
|
||||
loadRecaptcha(window._recaptchaSiteKey);
|
||||
}
|
||||
|
||||
// Load settings from settings storage now all the controls are available
|
||||
loadSettings();
|
||||
|
||||
// Update the upstream area for any pre-selected SIG
|
||||
updateUpstreamArea();
|
||||
});
|
||||
}
|
||||
|
||||
// Load and inject the reCAPTCHA script
|
||||
function loadRecaptcha(siteKey) {
|
||||
window._recaptchaSiteKey = siteKey;
|
||||
if (!document.getElementById('recaptcha-script')) {
|
||||
const script = document.createElement('script');
|
||||
script.id = 'recaptcha-script';
|
||||
script.src = 'https://www.google.com/recaptcha/api.js?render=explicit&onload=renderRecaptcha';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
$("#recaptcha-area").show();
|
||||
}
|
||||
|
||||
// Called by reCAPTCHA after its script loads
|
||||
function renderRecaptcha() {
|
||||
window._recaptchaWidgetId = grecaptcha.render('recaptcha-widget', {
|
||||
sitekey: window._recaptchaSiteKey,
|
||||
size: 'normal'
|
||||
});
|
||||
}
|
||||
|
||||
// Update the "Send spot to..." area based on the currently selected SIG
|
||||
function updateUpstreamArea() {
|
||||
if (!window._allowUpstreamSpotting || !options || !options["spot_submit_providers"]) {
|
||||
$("#upstream-area").hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const sig = $("#sig").val();
|
||||
const providers = (sig && options["spot_submit_providers"][sig]) ? options["spot_submit_providers"][sig] : [];
|
||||
|
||||
if (providers.length === 0) {
|
||||
$("#upstream-area").hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$("#upstream-area").show();
|
||||
|
||||
// Update the provider selector
|
||||
$("#upstream-provider-select").empty();
|
||||
$.each(providers, function (i, name) {
|
||||
$("#upstream-provider-select").append($('<option>', {value: name, text: name}));
|
||||
});
|
||||
|
||||
if (providers.length > 1) {
|
||||
$("#upstream-provider-label").text("upstream spot sources:");
|
||||
$("#upstream-provider-select-col").show();
|
||||
} else {
|
||||
$("#upstream-provider-label").text(providers[0]);
|
||||
$("#upstream-provider-select-col").hide();
|
||||
}
|
||||
|
||||
// Show the credentials button if this provider has an authentication mechanism and we need input from the user
|
||||
updateCredentialsButton();
|
||||
}
|
||||
|
||||
// Update the credentials button visibility based on selected provider
|
||||
function updateCredentialsButton() {
|
||||
const providerName = getSelectedUpstreamProvider();
|
||||
if (providerName && PROVIDER_CREDENTIAL_SCHEMAS[providerName]) {
|
||||
$("#upstream-credentials-btn").show();
|
||||
} else {
|
||||
$("#upstream-credentials-btn").hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Get the currently selected upstream provider name
|
||||
function getSelectedUpstreamProvider() {
|
||||
const providers = (options && options["spot_submit_providers"] && $("#sig").val())
|
||||
? (options["spot_submit_providers"][$("#sig").val()] || [])
|
||||
: [];
|
||||
if (providers.length === 0) return null;
|
||||
if (providers.length === 1) return providers[0];
|
||||
return $("#upstream-provider-select").val();
|
||||
}
|
||||
|
||||
// Show the credentials modal for the currently selected upstream provider
|
||||
function showCredentialsModal() {
|
||||
const providerName = getSelectedUpstreamProvider();
|
||||
if (!providerName || !PROVIDER_CREDENTIAL_SCHEMAS[providerName]) return;
|
||||
|
||||
const schema = PROVIDER_CREDENTIAL_SCHEMAS[providerName];
|
||||
const stored = loadCredentials(providerName);
|
||||
|
||||
$("#credentials-provider-name").text(providerName);
|
||||
$("#credentials-fields").empty();
|
||||
|
||||
$.each(schema, function (i, field) {
|
||||
const val = stored[field.key] || "";
|
||||
let html = '<div class="mb-3">';
|
||||
html += '<label for="cred-' + field.key + '" class="form-label">' + field.label + '</label>';
|
||||
html += '<input type="text" class="form-control" id="cred-' + field.key + '" value="' + $('<div>').text(val).html() + '">';
|
||||
if (field.help) {
|
||||
html += '<div class="form-text">' + field.help + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
$("#credentials-fields").append(html);
|
||||
});
|
||||
|
||||
// Store provider name for saveCredentials()
|
||||
$("#credentials-modal").data("provider", providerName);
|
||||
new bootstrap.Modal(document.getElementById('credentials-modal')).show();
|
||||
}
|
||||
|
||||
// Save credentials from the modal to local storage
|
||||
function saveCredentials() {
|
||||
const providerName = $("#credentials-modal").data("provider");
|
||||
if (!providerName || !PROVIDER_CREDENTIAL_SCHEMAS[providerName]) return;
|
||||
|
||||
const schema = PROVIDER_CREDENTIAL_SCHEMAS[providerName];
|
||||
const creds = {};
|
||||
$.each(schema, function (i, field) {
|
||||
creds[field.key] = $("#cred-" + field.key).val();
|
||||
});
|
||||
localStorage.setItem("upstream-credentials-" + providerName, JSON.stringify(creds));
|
||||
bootstrap.Modal.getInstance(document.getElementById('credentials-modal')).hide();
|
||||
}
|
||||
|
||||
// Load credentials for a provider from local storage
|
||||
function loadCredentials(providerName) {
|
||||
const stored = localStorage.getItem("upstream-credentials-" + providerName);
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
}
|
||||
|
||||
// Method called to add a spot to the server
|
||||
function addSpot() {
|
||||
try {
|
||||
// Save settings (this will save "your call" for future use)
|
||||
saveSettings();
|
||||
|
||||
// Unpack the user's entered values
|
||||
const dx = $("#dx-call").val().toUpperCase();
|
||||
const freqStr = $("#freq").val();
|
||||
const mode = $("#mode")[0].value;
|
||||
const sig = $("#sig")[0].value;
|
||||
const sigRef = $("#sig-ref").val();
|
||||
const dxGrid = $("#dx-grid").val();
|
||||
const comment = $("#comment").val();
|
||||
const de = $("#de-call").val().toUpperCase();
|
||||
|
||||
// Prepare the spot object for the server
|
||||
const spot = {};
|
||||
spot["dx_call"] = dx;
|
||||
spot["freq"] = parseFloat(freqStr) * 1000;
|
||||
if (mode !== "") spot["mode"] = mode;
|
||||
if (sig !== "") spot["sig"] = sig;
|
||||
if (sigRef !== "") spot["sig_refs"] = [{sig: sig, id: sigRef}];
|
||||
if (dxGrid !== "") spot["dx_grid"] = dxGrid;
|
||||
if (comment !== "") spot["comment"] = comment;
|
||||
spot["de_call"] = de;
|
||||
spot["time"] = moment.utc().valueOf() / 1000.0;
|
||||
|
||||
// Prepare "handling" structure to tell the server what to do with this spot
|
||||
const handling = {};
|
||||
|
||||
// Add CAPTCHA token if reCAPTCHA is loaded
|
||||
if (window._recaptchaWidgetId !== undefined) {
|
||||
handling["captcha_token"] = grecaptcha.getResponse(window._recaptchaWidgetId);
|
||||
}
|
||||
|
||||
// Upstream submission
|
||||
const submitUpstream = $("#submit-upstream").is(":checked");
|
||||
const upstreamProviderName = getSelectedUpstreamProvider();
|
||||
if (submitUpstream && upstreamProviderName) {
|
||||
handling["submit_upstream"] = true;
|
||||
handling["upstream_provider"] = upstreamProviderName;
|
||||
handling["upstream_credentials"] = loadCredentials(upstreamProviderName);
|
||||
}
|
||||
|
||||
$.ajax("/api/v2/spot", {
|
||||
data: JSON.stringify({spot, handling}),
|
||||
contentType: 'application/json',
|
||||
type: 'POST',
|
||||
timeout: 10000,
|
||||
success: async function (result) {
|
||||
// Reset CAPTCHA for next use
|
||||
if (window._recaptchaWidgetId !== undefined) {
|
||||
grecaptcha.reset(window._recaptchaWidgetId);
|
||||
}
|
||||
if (result && result.startsWith && result.startsWith("Warning")) {
|
||||
$("#result-good").html("<div class='alert alert-warning fade show mb-0 mt-4' role='alert'><i class='fa-solid fa-triangle-exclamation'></i> " + result + " Returning you to the spots list...</div>");
|
||||
} else {
|
||||
$("#result-good").html("<div class='alert alert-success fade show mb-0 mt-4' role='alert'><i class='fa-solid fa-check'></i> Spot submitted. Returning you to the spots list...</div>");
|
||||
}
|
||||
$("#result-bad").html("");
|
||||
setTimeout(() => {
|
||||
$("#result-good").hide();
|
||||
window.location.replace("/");
|
||||
}, 2000);
|
||||
},
|
||||
error: function (result) {
|
||||
if (window._recaptchaWidgetId !== undefined) {
|
||||
grecaptcha.reset(window._recaptchaWidgetId);
|
||||
}
|
||||
if (result.responseText) {
|
||||
showAddSpotError(result.responseText.slice(1, -1));
|
||||
} else {
|
||||
showAddSpotError("The server did not return a response.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
showAddSpotError(error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show an "add spot" error.
|
||||
function showAddSpotError(text) {
|
||||
const div = $("<div class='alert alert-danger alert-dismissible fade show mb-0 mt-4' role='alert'></div>");
|
||||
div.append("<i class='fa-solid fa-triangle-exclamation'></i> ");
|
||||
div.append(document.createTextNode(text));
|
||||
div.append("<button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Close'></button>");
|
||||
$("#result-bad").empty().append(div);
|
||||
}
|
||||
|
||||
// Force callsign and mode capitalisation
|
||||
$("#dx-call").change(function () {
|
||||
$(this).val($(this).val().trim().toUpperCase());
|
||||
});
|
||||
$("#de-call").change(function () {
|
||||
$(this).val($(this).val().trim().toUpperCase());
|
||||
});
|
||||
$("#mode").change(function () {
|
||||
$(this).val($(this).val().trim().toUpperCase());
|
||||
});
|
||||
|
||||
// Update upstream area and credentials button when SIG changes
|
||||
$("#sig").change(function () {
|
||||
updateUpstreamArea();
|
||||
});
|
||||
|
||||
// Update credentials button when provider selector changes
|
||||
$("#upstream-provider-select").change(function () {
|
||||
updateCredentialsButton();
|
||||
});
|
||||
|
||||
// Startup
|
||||
$(document).ready(function () {
|
||||
// Load options
|
||||
loadOptions();
|
||||
});
|
||||
Reference in New Issue
Block a user