mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2026-10-25 14:20:05 -05:00
Merge pull request #1017 from LibreQoE/insight_signup_flow
Feat: add Insight signup handoff
This commit is contained in:
10 files changed
+593
-66
No files matched your search
@@ -14,6 +14,7 @@ flow_map.js
|
||||
all_tree_sankey.js
|
||||
asn_analysis.js
|
||||
lts_trial.js
|
||||
lts_trial_setup.js
|
||||
config_general.js
|
||||
config_rtt.js
|
||||
config_tuning.js
|
||||
|
||||
@@ -1,37 +1,5 @@
|
||||
import {PLACEHOLDER_TEASERS} from "./lts_teasers_shared";
|
||||
import {get_ws_client} from "./pubsub/ws";
|
||||
|
||||
const wsClient = get_ws_client();
|
||||
const listenOnce = (eventName, handler) => {
|
||||
const wrapped = (msg) => {
|
||||
wsClient.off(eventName, wrapped);
|
||||
handler(msg);
|
||||
};
|
||||
wsClient.on(eventName, wrapped);
|
||||
};
|
||||
|
||||
function sendWsRequest(responseEvent, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let done = false;
|
||||
const onResponse = (msg) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
wsClient.off(responseEvent, onResponse);
|
||||
wsClient.off("Error", onError);
|
||||
resolve(msg);
|
||||
};
|
||||
const onError = (msg) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
wsClient.off(responseEvent, onResponse);
|
||||
wsClient.off("Error", onError);
|
||||
reject(msg);
|
||||
};
|
||||
wsClient.on(responseEvent, onResponse);
|
||||
wsClient.on("Error", onError);
|
||||
wsClient.send(request);
|
||||
});
|
||||
}
|
||||
import {buildSignupApiUrl, sendWsRequest, wsClient} from "./lts_trial_shared";
|
||||
|
||||
// Paddle-compatible countries (excluding embargoed nations)
|
||||
const ALLOWED_COUNTRIES = [
|
||||
@@ -288,6 +256,7 @@ $(document).ready(async function() {
|
||||
loadTeasers(); // Then load teasers with the correct URL
|
||||
fetchCircuitCount();
|
||||
attachEventHandlers();
|
||||
openInitialViewFromQuery();
|
||||
|
||||
// Apply dark mode if needed
|
||||
if (isDarkMode()) {
|
||||
@@ -434,7 +403,7 @@ function displayTeasers() {
|
||||
|
||||
// Add click handlers to CTA buttons
|
||||
$('.btn-teaser-cta').on('click', function() {
|
||||
showSection('signupSection');
|
||||
redirectToTrialSetup();
|
||||
});
|
||||
|
||||
// Reinitialize carousel if needed
|
||||
@@ -448,19 +417,7 @@ function displayTeasers() {
|
||||
|
||||
// Helper function to construct full LTS URLs
|
||||
function getLtsUrl(endpoint) {
|
||||
// Ensure ltsBaseUrl starts with http:// or https://, otherwise prefix with https://
|
||||
let baseUrl = ltsBaseUrl;
|
||||
if (!/^https?:\/\//i.test(baseUrl)) {
|
||||
baseUrl = 'https://' + baseUrl;
|
||||
}
|
||||
// Ensure baseUrl ends with a single slash
|
||||
let base = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/';
|
||||
// Ensure 'signup-api/' is appended exactly once
|
||||
base += base.endsWith('signup-api/') ? '' : 'signup-api/';
|
||||
// Remove any leading slash from endpoint
|
||||
endpoint = endpoint.replace(/^\/+/, '');
|
||||
//console.log("Base: ", base, "Endpoint:", endpoint);
|
||||
return base + endpoint;
|
||||
return buildSignupApiUrl(ltsBaseUrl, endpoint);
|
||||
}
|
||||
|
||||
// Fetch node ID and LTS URL from configuration
|
||||
@@ -517,6 +474,29 @@ function showSection(sectionId) {
|
||||
$(`#${sectionId}`).fadeIn();
|
||||
}
|
||||
|
||||
function redirectToTrialSetup() {
|
||||
window.location.href = 'lts_trial_setup.html';
|
||||
}
|
||||
|
||||
function openInitialViewFromQuery() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const view = params.get('view');
|
||||
|
||||
if (view === 'signup') {
|
||||
showSection('signupSection');
|
||||
return;
|
||||
}
|
||||
|
||||
if (view === 'license') {
|
||||
showSection('licenseKeySection');
|
||||
return;
|
||||
}
|
||||
|
||||
if (view === 'recover') {
|
||||
showSection('licenseRecoverySection');
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading modal
|
||||
function showLoading(message = 'Processing...') {
|
||||
$('#loadingMessage').text(message);
|
||||
@@ -579,7 +559,7 @@ function attachEventHandlers() {
|
||||
// Analytics tracking for new customer path
|
||||
const trackingImg = new Image();
|
||||
trackingImg.src = getLtsUrl('signupPing') + '?t=' + Date.now() + '&type=new';
|
||||
showSection('signupSection');
|
||||
redirectToTrialSetup();
|
||||
});
|
||||
$('#btnBackFromLicense').on('click', () => showSection('teaserSection'));
|
||||
$('#btnBackFromSignup').on('click', () => showSection('teaserSection'));
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import {buildInsightUrl, sendWsRequest} from "./lts_trial_shared";
|
||||
|
||||
const setupStatusClasses = [
|
||||
'alert-primary',
|
||||
'alert-success',
|
||||
'alert-warning',
|
||||
'alert-danger',
|
||||
'alert-secondary',
|
||||
];
|
||||
const defaultInsightBaseUrl = 'https://insight.libreqos.com/';
|
||||
let insightBaseUrl = defaultInsightBaseUrl;
|
||||
|
||||
function setSetupStatus(message, tone = 'primary', spinning = true) {
|
||||
const status = $('#signupSetupStatus');
|
||||
const statusText = $('#signupSetupStatusText');
|
||||
const spinner = status.find('.spinner-border');
|
||||
|
||||
if (!status.length || !statusText.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
status.removeClass(setupStatusClasses.join(' ')).addClass(`alert-${tone}`);
|
||||
statusText.text(message);
|
||||
|
||||
if (spinning) {
|
||||
spinner.show();
|
||||
} else {
|
||||
spinner.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function setSetupHelp(message) {
|
||||
$('#signupSetupHelp').text(message);
|
||||
}
|
||||
|
||||
function getErrorMessage(error, fallback) {
|
||||
if (error && typeof error.message === 'string' && error.message.trim().length > 0) {
|
||||
return error.message.trim();
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function fetchTrialConfig() {
|
||||
try {
|
||||
const response = await sendWsRequest("LtsTrialConfigResult", { LtsTrialConfig: {} });
|
||||
const data = response && response.data ? response.data : {};
|
||||
if (typeof data.lts_url === 'string' && data.lts_url.trim().length > 0) {
|
||||
insightBaseUrl = data.lts_url;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Unable to load LTS trial config, using default Insight URL.', error);
|
||||
insightBaseUrl = defaultInsightBaseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function redirectToInsightSignup(claimId) {
|
||||
window.location.href = buildInsightUrl(
|
||||
insightBaseUrl,
|
||||
`su/signup1.html?${encodeURIComponent(claimId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function startSignupSession() {
|
||||
setSetupStatus('Creating your Insight signup session…');
|
||||
setSetupHelp('LibreQoS is contacting Insight and preparing your claim.');
|
||||
await fetchTrialConfig();
|
||||
|
||||
try {
|
||||
const response = await sendWsRequest("LtsStartSignupResult", { LtsStartSignup: {} });
|
||||
const claimId = response && typeof response.claim_id === 'string'
|
||||
? response.claim_id.trim()
|
||||
: '';
|
||||
|
||||
if (!claimId) {
|
||||
throw new Error('Missing claim ID from signup response.');
|
||||
}
|
||||
|
||||
setSetupStatus('Signup session ready. Redirecting to Insight…', 'success', false);
|
||||
setSetupHelp('Your signup session is ready. Continuing to Insight now.');
|
||||
redirectToInsightSignup(claimId);
|
||||
} catch (error) {
|
||||
console.error('Failed to start Insight signup session.', error);
|
||||
setSetupStatus(
|
||||
getErrorMessage(error, 'Unable to start the Insight signup session right now.'),
|
||||
'danger',
|
||||
false,
|
||||
);
|
||||
setSetupHelp(
|
||||
'Retry this page, use the direct signup form, or validate an existing license key below.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
setSetupStatus('Preparing the Insight signup handoff…');
|
||||
void startSignupSession();
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import {get_ws_client} from "./pubsub/ws";
|
||||
|
||||
const DEFAULT_INSIGHT_BASE_URL = 'https://insight.libreqos.com/';
|
||||
const wsClient = get_ws_client();
|
||||
|
||||
function normalizeInsightBaseUrl(rawBaseUrl) {
|
||||
let baseUrl = (rawBaseUrl || DEFAULT_INSIGHT_BASE_URL).trim();
|
||||
|
||||
if (!baseUrl) {
|
||||
baseUrl = DEFAULT_INSIGHT_BASE_URL;
|
||||
}
|
||||
|
||||
if (!/^https?:\/\//i.test(baseUrl)) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
baseUrl = baseUrl.replace(/\/+$/, '');
|
||||
if (baseUrl.endsWith('/signup-api')) {
|
||||
baseUrl = baseUrl.slice(0, -'/signup-api'.length);
|
||||
}
|
||||
|
||||
return `${baseUrl}/`;
|
||||
}
|
||||
|
||||
function buildInsightUrl(rawBaseUrl, endpoint) {
|
||||
const path = String(endpoint || '').replace(/^\/+/, '');
|
||||
return `${normalizeInsightBaseUrl(rawBaseUrl)}${path}`;
|
||||
}
|
||||
|
||||
function buildSignupApiUrl(rawBaseUrl, endpoint) {
|
||||
const path = String(endpoint || '').replace(/^\/+/, '');
|
||||
return buildInsightUrl(rawBaseUrl, `signup-api/${path}`);
|
||||
}
|
||||
|
||||
function sendWsRequest(responseEvent, request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let done = false;
|
||||
const onResponse = (msg) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
wsClient.off(responseEvent, onResponse);
|
||||
wsClient.off("Error", onError);
|
||||
resolve(msg);
|
||||
};
|
||||
const onError = (msg) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
wsClient.off(responseEvent, onResponse);
|
||||
wsClient.off("Error", onError);
|
||||
reject(msg);
|
||||
};
|
||||
wsClient.on(responseEvent, onResponse);
|
||||
wsClient.on("Error", onError);
|
||||
wsClient.send(request);
|
||||
});
|
||||
}
|
||||
|
||||
export {buildInsightUrl, buildSignupApiUrl, normalizeInsightBaseUrl, sendWsRequest, wsClient};
|
||||
@@ -1,8 +1,10 @@
|
||||
mod last_24_hours;
|
||||
mod shaper_status;
|
||||
|
||||
use crate::lts2_sys::lts2_client::{LicenseStatus, set_license_status};
|
||||
use crate::lts2_sys::shared_types::LtsStatus;
|
||||
use crate::node_manager::auth::LoginResult;
|
||||
use crate::node_manager::local_api::circuit_count;
|
||||
use axum::http::StatusCode;
|
||||
pub use last_24_hours::*;
|
||||
use lqos_bus::{BusRequest, bus_request};
|
||||
@@ -10,9 +12,14 @@ use lqos_config::load_config;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use shaper_status::ShaperStatus;
|
||||
pub use shaper_status::shaper_status_data;
|
||||
use std::ops::Deref;
|
||||
use std::process::Command;
|
||||
use tracing::{info, warn};
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
const SIGNUP_POLL_INTERVAL: Duration = Duration::from_secs(10);
|
||||
const INSIGHT_FREE_TRIAL_STATUS_CODE: i32 = 2;
|
||||
const INSIGHT_TRIAL_DAYS_HINT: i32 = 30;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LtsTrialConfig {
|
||||
@@ -20,6 +27,37 @@ pub struct LtsTrialConfig {
|
||||
pub lts_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SignupStartRequest {
|
||||
node_id: String,
|
||||
node_name: String,
|
||||
active_circuits: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SignupStartResponse {
|
||||
claim_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SignupCheckRequest {
|
||||
claim_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SignupCheckResponse {
|
||||
status: String,
|
||||
#[serde(default)]
|
||||
account_key: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SignupCheckState {
|
||||
Pending,
|
||||
Provisioned { account_key: Uuid },
|
||||
Expired,
|
||||
}
|
||||
|
||||
pub fn lts_trial_config_data(login: LoginResult) -> Result<LtsTrialConfig, StatusCode> {
|
||||
if login != LoginResult::Admin {
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
@@ -31,6 +69,221 @@ pub fn lts_trial_config_data(login: LoginResult) -> Result<LtsTrialConfig, Statu
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_insight_base_url(lts_url: Option<&str>) -> String {
|
||||
let mut base_url = lts_url
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("https://insight.libreqos.com/")
|
||||
.to_string();
|
||||
|
||||
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
|
||||
base_url = format!("https://{base_url}");
|
||||
}
|
||||
|
||||
while base_url.ends_with('/') {
|
||||
base_url.pop();
|
||||
}
|
||||
|
||||
if let Some(stripped) = base_url.strip_suffix("/signup-api") {
|
||||
base_url = stripped.to_string();
|
||||
}
|
||||
|
||||
format!("{base_url}/")
|
||||
}
|
||||
|
||||
fn build_insight_url(lts_url: Option<&str>, path: &str) -> String {
|
||||
let path = path.trim_start_matches('/');
|
||||
format!("{}{}", normalize_insight_base_url(lts_url), path)
|
||||
}
|
||||
|
||||
fn insight_http_client() -> Result<reqwest::Client, StatusCode> {
|
||||
reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.timeout(Duration::from_secs(15))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
fn classify_signup_check_response(response: SignupCheckResponse) -> Result<SignupCheckState, ()> {
|
||||
match response.status.as_str() {
|
||||
"pending" => Ok(SignupCheckState::Pending),
|
||||
"expired" => Ok(SignupCheckState::Expired),
|
||||
"provisioned" => response
|
||||
.account_key
|
||||
.map(|account_key| SignupCheckState::Provisioned { account_key })
|
||||
.ok_or(()),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn apply_insight_license(license_key: String, restart_lqos_api: bool) -> Result<(), StatusCode> {
|
||||
let license_key = license_key.trim().to_string();
|
||||
if license_key.is_empty() {
|
||||
return Err(StatusCode::BAD_GATEWAY);
|
||||
}
|
||||
|
||||
let mut cfg = load_config()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.as_ref()
|
||||
.clone();
|
||||
cfg.long_term_stats.gather_stats = true;
|
||||
cfg.long_term_stats.license_key = Some(license_key.clone());
|
||||
bus_request(vec![BusRequest::UpdateLqosdConfig(Box::new(cfg))])
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
set_license_status(LicenseStatus {
|
||||
license_type: INSIGHT_FREE_TRIAL_STATUS_CODE,
|
||||
trial_expires: INSIGHT_TRIAL_DAYS_HINT,
|
||||
});
|
||||
|
||||
info!("LQOSD configuration updated with new Insight license key.");
|
||||
|
||||
if restart_lqos_api {
|
||||
let _ = Command::new("/bin/systemctl")
|
||||
.args(["restart", "lqos_api"])
|
||||
.output();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn lts_trial_start_signup_data() -> Result<String, StatusCode> {
|
||||
let config = load_config().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let start_url = build_insight_url(config.long_term_stats.lts_url.as_deref(), "/su/start");
|
||||
let request = SignupStartRequest {
|
||||
node_id: config.node_id.clone(),
|
||||
node_name: config.node_name.clone(),
|
||||
active_circuits: circuit_count::circuit_count_data().count,
|
||||
};
|
||||
debug!(
|
||||
node_id = %request.node_id,
|
||||
active_circuits = request.active_circuits,
|
||||
url = %start_url,
|
||||
"starting Insight signup session"
|
||||
);
|
||||
|
||||
let client = insight_http_client()?;
|
||||
|
||||
let response = client
|
||||
.post(&start_url)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| StatusCode::BAD_GATEWAY)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
warn!(
|
||||
status = %response.status(),
|
||||
url = %start_url,
|
||||
"Insight signup start request failed"
|
||||
);
|
||||
return Err(StatusCode::BAD_GATEWAY);
|
||||
}
|
||||
|
||||
let response = response
|
||||
.json::<SignupStartResponse>()
|
||||
.await
|
||||
.map_err(|_| StatusCode::BAD_GATEWAY)?;
|
||||
|
||||
Ok(response.claim_id.to_string())
|
||||
}
|
||||
|
||||
pub fn spawn_signup_poll_loop(claim_id: String) {
|
||||
tokio::spawn(async move {
|
||||
if let Err(status) = poll_signup_until_complete(claim_id.clone()).await {
|
||||
warn!(
|
||||
claim_id = %claim_id,
|
||||
status = %status,
|
||||
"Insight signup poll loop stopped with an error"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn poll_signup_until_complete(claim_id: String) -> Result<(), StatusCode> {
|
||||
let config = load_config().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let check_url = build_insight_url(config.long_term_stats.lts_url.as_deref(), "/su/check");
|
||||
let client = insight_http_client()?;
|
||||
let request = SignupCheckRequest {
|
||||
claim_id: claim_id.clone(),
|
||||
};
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(SIGNUP_POLL_INTERVAL).await;
|
||||
|
||||
let response = match client.post(&check_url).json(&request).send().await {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
claim_id = %claim_id,
|
||||
error = ?error,
|
||||
"Insight signup status poll failed; retrying"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if !response.status().is_success() {
|
||||
warn!(
|
||||
claim_id = %claim_id,
|
||||
status = %response.status(),
|
||||
"Insight signup status poll returned non-success; retrying"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let response = match response.json::<SignupCheckResponse>().await {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
claim_id = %claim_id,
|
||||
error = ?error,
|
||||
"Insight signup status poll returned invalid JSON; retrying"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match classify_signup_check_response(response) {
|
||||
Ok(SignupCheckState::Pending) => {}
|
||||
Ok(SignupCheckState::Expired) => {
|
||||
debug!(claim_id = %claim_id, "Insight signup claim expired");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(SignupCheckState::Provisioned { account_key }) => {
|
||||
match apply_insight_license(account_key.to_string(), false).await {
|
||||
Ok(()) => {
|
||||
info!(
|
||||
claim_id = %claim_id,
|
||||
account_key = %account_key,
|
||||
"Applied Insight license from signup flow"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(status) => {
|
||||
warn!(
|
||||
claim_id = %claim_id,
|
||||
account_key = %account_key,
|
||||
status = %status,
|
||||
"Failed to apply Insight license; stopping signup poll loop"
|
||||
);
|
||||
return Err(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(()) => {
|
||||
warn!(
|
||||
claim_id = %claim_id,
|
||||
"Insight signup status poll returned an unexpected payload"
|
||||
);
|
||||
return Err(StatusCode::BAD_GATEWAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn insight_gate() -> Result<(), StatusCode> {
|
||||
let (status, _) = crate::lts2_sys::get_lts_license_status_async().await;
|
||||
match status {
|
||||
@@ -56,20 +309,83 @@ pub async fn lts_trial_signup_data(license_key: String) -> Result<(), StatusCode
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
info!("Free trial request succeeded, license key: {}", license_key);
|
||||
let mut cfg = load_config()
|
||||
.expect("Unable to load LibreQoS config")
|
||||
.deref()
|
||||
.clone();
|
||||
cfg.long_term_stats.gather_stats = true;
|
||||
cfg.long_term_stats.license_key = Some(license_key);
|
||||
bus_request(vec![BusRequest::UpdateLqosdConfig(Box::new(cfg))])
|
||||
.await
|
||||
.expect("Unable to update lqosd config");
|
||||
info!("LQOSD configuration updated with new license key.");
|
||||
// Best-effort: ensure the bundled lqos_api also reloads to pick up the new license
|
||||
// Ignore errors if systemctl isn't present or permission is denied.
|
||||
let _ = Command::new("/bin/systemctl")
|
||||
.args(["restart", "lqos_api"])
|
||||
.output();
|
||||
Ok(())
|
||||
apply_insight_license(license_key, true).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
SignupCheckResponse, SignupCheckState, build_insight_url, classify_signup_check_response,
|
||||
normalize_insight_base_url,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn normalize_insight_base_url_defaults_to_hosted_insight() {
|
||||
assert_eq!(
|
||||
normalize_insight_base_url(None),
|
||||
"https://insight.libreqos.com/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_insight_base_url_adds_scheme_and_trailing_slash() {
|
||||
assert_eq!(
|
||||
normalize_insight_base_url(Some("insight.example.com")),
|
||||
"https://insight.example.com/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_insight_base_url_strips_signup_api_suffix() {
|
||||
assert_eq!(
|
||||
normalize_insight_base_url(Some("https://insight.example.com/signup-api/")),
|
||||
"https://insight.example.com/"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_insight_url_joins_paths_cleanly() {
|
||||
assert_eq!(
|
||||
build_insight_url(Some("insight.example.com"), "/su/start"),
|
||||
"https://insight.example.com/su/start"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_signup_check_response_handles_pending() {
|
||||
let response = SignupCheckResponse {
|
||||
status: "pending".to_string(),
|
||||
account_key: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
classify_signup_check_response(response),
|
||||
Ok(SignupCheckState::Pending)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_signup_check_response_requires_account_key_for_provisioned() {
|
||||
let response = SignupCheckResponse {
|
||||
status: "provisioned".to_string(),
|
||||
account_key: None,
|
||||
};
|
||||
|
||||
assert!(classify_signup_check_response(response).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_signup_check_response_returns_provisioned_account_key() {
|
||||
let account_key = Uuid::new_v4();
|
||||
let response = SignupCheckResponse {
|
||||
status: "provisioned".to_string(),
|
||||
account_key: Some(account_key),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
classify_signup_check_response(response),
|
||||
Ok(SignupCheckState::Provisioned { account_key })
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,11 @@ body.dark-mode .card-header.bg-light {
|
||||
<button id="btnNewCustomer" class="btn btn-lg btn-outline-primary mb-3">
|
||||
<i class="fas fa-rocket"></i> Start my free trial
|
||||
</button>
|
||||
<div class="mt-2">
|
||||
<a id="lnkLegacySignup" class="link-secondary" href="lts_trial.html?view=signup">
|
||||
Need the direct signup form instead?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="row justify-content-center py-4">
|
||||
<div class="col-xl-7 col-lg-8">
|
||||
<div class="card shadow border-primary-subtle">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-satellite-dish text-primary"></i>
|
||||
Preparing Your Insight Sign-Up Session
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="lead mb-3">
|
||||
LibreQoS will create a short-lived Insight claim and then hand your browser to the hosted signup flow.
|
||||
</p>
|
||||
<div id="signupSetupStatus" class="alert alert-primary d-flex align-items-center mb-3" role="status">
|
||||
<span class="spinner-border spinner-border-sm text-primary me-2" aria-hidden="true"></span>
|
||||
<span id="signupSetupStatusText">Preparing the Insight signup handoff…</span>
|
||||
</div>
|
||||
<p id="signupSetupHelp" class="text-muted mb-4">
|
||||
Existing license validation and the direct signup form remain available if you need them.
|
||||
</p>
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<a class="btn btn-outline-success" href="lts_trial.html?view=license">
|
||||
<i class="fas fa-key"></i> I have a license key
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary" href="lts_trial.html?view=signup">
|
||||
<i class="fas fa-user-plus"></i> Use the direct signup form
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary" href="lts_trial.html">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="lts_trial_setup.js%CACHEBUSTERS%"></script>
|
||||
@@ -45,6 +45,7 @@ pub(super) fn static_routes() -> Result<Router> {
|
||||
"api_docs.html",
|
||||
"chatbot.html",
|
||||
"lts_trial.html",
|
||||
"lts_trial_setup.html",
|
||||
"lts_trial_success.html",
|
||||
"lts_trial_fail.html",
|
||||
"executive_worst_sites.html",
|
||||
|
||||
@@ -1036,6 +1036,34 @@ async fn receive_channel_message(
|
||||
return true;
|
||||
}
|
||||
}
|
||||
WsRequest::LtsStartSignup => {
|
||||
let result = lts::lts_trial_start_signup_data().await;
|
||||
match result {
|
||||
Ok(claim_id) => {
|
||||
lts::spawn_signup_poll_loop(claim_id.clone());
|
||||
let response = WsResponse::LtsStartSignupResult { claim_id };
|
||||
if send_ws_response(&tx, response).await {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Err(StatusCode::FORBIDDEN) => {
|
||||
let response = WsResponse::Error {
|
||||
message: "Unauthorized".to_string(),
|
||||
};
|
||||
if send_ws_response(&tx, response).await {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let response = WsResponse::Error {
|
||||
message: "Unable to start the Insight signup session.".to_string(),
|
||||
};
|
||||
if send_ws_response(&tx, response).await {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WsRequest::LtsSignUp { license_key } => {
|
||||
let result = lts::lts_trial_signup_data(license_key).await;
|
||||
let (ok, message) = match result {
|
||||
|
||||
@@ -132,6 +132,7 @@ pub enum WsRequest {
|
||||
ReloadLibreQoS,
|
||||
LtsTrialConfig,
|
||||
CircuitCount,
|
||||
LtsStartSignup,
|
||||
LtsSignUp {
|
||||
license_key: String,
|
||||
},
|
||||
@@ -693,6 +694,9 @@ pub enum WsResponse {
|
||||
CircuitCountResult {
|
||||
data: CircuitCount,
|
||||
},
|
||||
LtsStartSignupResult {
|
||||
claim_id: String,
|
||||
},
|
||||
LtsSignUpResult {
|
||||
ok: bool,
|
||||
message: String,
|
||||
|
||||
Reference in new issue
Block a user