mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-06-24 21:55:14 +00:00
308 lines
12 KiB
JavaScript
308 lines
12 KiB
JavaScript
// 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"] = [{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();
|
|
});
|