diff --git a/src/common/meta/http.rs b/src/common/meta/http.rs index 1cf3bdbe16..58f418002b 100644 --- a/src/common/meta/http.rs +++ b/src/common/meta/http.rs @@ -121,6 +121,24 @@ impl HttpResponse { .into_response() } + pub fn created(msg: impl ToString) -> Response { + ( + StatusCode::CREATED, + Json(Self::message(StatusCode::CREATED, msg.to_string())), + ) + .into_response() + } + + pub fn found(loc: impl ToString, cookie: Option) -> Response { + let mut builder = Response::builder() + .status(StatusCode::FOUND) + .header("Location", loc.to_string()); + if let Some(c) = cookie { + builder = builder.header("Set-Cookie", c) + } + builder.body(().into()).unwrap() + } + /// Send a BadRequest response in json format and associate the /// provided error as `error` field. pub fn bad_request(error: impl ToString) -> Response { diff --git a/src/handler/http/models/billings.rs b/src/handler/http/models/billings.rs index 9c9415d8a6..c0d2cd7805 100644 --- a/src/handler/http/models/billings.rs +++ b/src/handler/http/models/billings.rs @@ -39,6 +39,8 @@ pub struct ListSubscriptionResponseBody { pub subscription_type: String, #[serde(skip_serializing_if = "Option::is_none")] pub customer_id: Option, + /// Billing provider: "stripe" or "aws" + pub provider: String, } impl From for ListSubscriptionResponseBody { @@ -46,6 +48,7 @@ impl From for ListSubscriptionResponseBody { Self { subscription_type: value.subscription_type.to_string(), customer_id: value.customer_id, + provider: value.provider.to_string(), } } } diff --git a/src/handler/http/request/cloud/aws_marketplace.rs b/src/handler/http/request/cloud/aws_marketplace.rs new file mode 100644 index 0000000000..f81be9ae06 --- /dev/null +++ b/src/handler/http/request/cloud/aws_marketplace.rs @@ -0,0 +1,368 @@ +// Copyright 2025 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use axum::{ + extract::{Path, Query}, + response::Response, +}; +use axum_extra::extract::cookie::{Cookie, SameSite}; +use config::meta::user::UserRole; +use o2_enterprise::enterprise::aws_marketplace::{api as aws_mp_api, db as aws_mp_db}; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{meta::http::HttpResponse as MetaHttpResponse, utils::auth::UserEmail}, + handler::http::extractors::Headers, + service::{organization, users}, +}; + +/// Form data from AWS Marketplace POST +#[derive(Debug, Deserialize)] +pub struct AwsMarketplaceRegistrationForm { + #[serde(rename = "x-amzn-marketplace-token")] + pub token: String, + #[serde(rename = "x-amzn-marketplace-agreement-id")] + pub agreement_id: Option, + #[serde(rename = "x-amzn-marketplace-product-id")] + pub product_id: Option, + #[serde(rename = "x-amzn-marketplace-fulfillment-url")] + pub fulfillment_url: Option, +} + +/// AwsMarketplaceRegistration - Entry point from AWS Marketplace +/// +/// This endpoint receives the POST from AWS Marketplace when a customer subscribes. +/// It saves the token in a cookie and redirects to the login page. +/// Full path: POST /marketplace/aws/register +pub async fn aws_marketplace_register( + axum::Form(form): axum::Form, +) -> Response { + log::info!( + "[AWS SAAS] Received marketplace registration: product_id={:?}, agreement_id={:?}", + form.product_id, + form.agreement_id + ); + + // Get the Dex login URL + let login_url = { + use o2_dex::service::auth::get_dex_login; + let login_data = get_dex_login(); + + // Store PKCE state + let state = login_data.state.clone(); + if let Err(e) = crate::service::kv::set( + crate::handler::http::auth::validator::PKCE_STATE_ORG, + &state, + state.clone().into(), + ) + .await + { + log::error!("[AWS SAAS] Failed to store PKCE state: {}", e); + } + + login_data.url + }; + + // Create cookie with the marketplace token + // Use SameSite::Lax to allow the cookie to be sent on redirect from Dex + let mut cookie = Cookie::new("aws_marketplace_token", form.token.clone()); + cookie.set_path("/"); + cookie.set_http_only(false); // Allow JavaScript to read it + cookie.set_secure(true); + cookie.set_same_site(SameSite::Lax); + cookie.set_expires(time::OffsetDateTime::now_utc() + time::Duration::hours(4)); // Token expires in 4 hours + + log::info!("[AWS SAAS] Redirecting to login: {}", login_url); + + MetaHttpResponse::found(login_url, Some(cookie.to_string())) +} + +/// Request payload for linking AWS Marketplace subscription +#[derive(Debug, Deserialize, utoipa::ToSchema)] +pub struct LinkSubscriptionRequest { + pub token: String, +} + +/// Response from link-subscription endpoint +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct LinkSubscriptionResponse { + pub success: bool, + pub customer_identifier: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +/// LinkAwsMarketplaceSubscription +#[utoipa::path( + post, + path="/{org_id}/aws-marketplace/link-subscription", + context_path = "/api", + tag = "AWS Marketplace", + operation_id = "LinkAwsMarketplaceSubscription", + summary = "Link AWS Marketplace subscription to organization", + description = "Exchanges AWS Marketplace token for customer info and creates pending subscription", + security( + ("Authorization" = []) + ), + request_body(content = LinkSubscriptionRequest), + responses( + (status = 200, description = "Success", content_type = "application/json", body = LinkSubscriptionResponse), + (status = 400, description = "Bad Request (token expired, invalid)", content_type = "application/json", body = ()), + (status = 403, description = "Forbidden (not admin)", content_type = "application/json", body = ()), + (status = 409, description = "Conflict (already subscribed)", content_type = "application/json", body = ()), + (status = 500, description = "Internal Server Error", content_type = "application/json", body = ()), + ), +)] +pub async fn link_subscription( + Path(org_id): Path, + Headers(user_email): Headers, + axum::Json(req): axum::Json, +) -> Response { + let email = user_email.user_id.as_str(); + let org_id = org_id.as_str(); + + log::info!( + "[AWS SAAS] Link subscription request: org_id={}, user={}", + org_id, + email + ); + + // 1. Validate organization exists + let _org = match organization::get_org(org_id).await { + Some(org) => org, + None => { + return MetaHttpResponse::not_found("Organization not found"); + } + }; + + // 2. Verify user is admin of the organization + let user = match users::get_user(Some(org_id), email).await { + Some(u) => u, + None => { + return MetaHttpResponse::forbidden( + "User not found or not authorized for this organization", + ); + } + }; + + // Only admins and root users can link AWS Marketplace subscriptions + if user.role != UserRole::Admin && user.role != UserRole::Root { + return MetaHttpResponse::forbidden( + "Only organization administrators can link AWS Marketplace subscriptions", + ); + } + + // 3. Check if org is free tier (cannot link paid orgs) + match organization::is_org_in_free_trial_period(org_id).await { + Ok(false) => { + return MetaHttpResponse::bad_request( + "Only free tier organizations can be linked to AWS Marketplace", + ); + } + Err(e) => { + log::error!("[AWS SAAS] Error checking free trial status: {}", e); + return MetaHttpResponse::internal_error("Failed to check organization status"); + } + Ok(true) => {} // Continue - org is in free trial + } + + // 4. Check if org is already linked to marketplace + match aws_mp_db::get_by_org_id(org_id).await { + Ok(Some(existing)) + if existing.marketplace_status == aws_mp_api::MarketplaceStatus::Active => + { + return MetaHttpResponse::conflict( + "This organization is already linked to AWS Marketplace", + ); + } + Err(e) => { + log::error!("[AWS SAAS] Database error checking org: {}", e); + return MetaHttpResponse::internal_error("Failed to check organization status"); + } + _ => {} // OK to proceed + } + + // 5. Resolve AWS Marketplace token + let resolved = match aws_mp_api::resolve_customer_token(&req.token).await { + Ok(customer) => customer, + Err(e) => { + log::error!("[AWS SAAS] Failed to resolve customer token: {}", e); + + if e.to_string().contains("TokenExpired") || e.to_string().contains("expired") { + return MetaHttpResponse::bad_request( + "Your registration link has expired. Please subscribe again from AWS Marketplace.", + ); + } + return MetaHttpResponse::internal_error(format!( + "Failed to validate AWS Marketplace token: {}", + e + )); + } + }; + + // 6. Check if customer_identifier already exists + match aws_mp_db::get_by_marketplace_customer_id(&resolved.customer_identifier).await { + Ok(Some(existing)) => { + use o2_enterprise::enterprise::aws_marketplace::db::ExistingSubscriptionAction; + + let action = aws_mp_db::handle_existing_subscription( + &existing, + org_id, + &resolved.customer_identifier, + ); + + return match action { + ExistingSubscriptionAction::AlreadySubscribed { + customer_identifier, + } => MetaHttpResponse::json(serde_json::json!({ + "success": true, + "customer_identifier": customer_identifier, + "message": "You are already subscribed!" + })), + ExistingSubscriptionAction::LinkedToDifferentOrg { existing_org_id } => { + MetaHttpResponse::conflict(format!( + "This AWS account is already linked to organization '{existing_org_id}'. Each AWS account can only have one active subscription.", + )) + } + ExistingSubscriptionAction::PendingActivation { + customer_identifier, + } => MetaHttpResponse::json(serde_json::json!({ + "success": true, + "customer_identifier": customer_identifier, + "message": "Subscription is being activated..." + })), + ExistingSubscriptionAction::StalePending => MetaHttpResponse::conflict( + "A pending subscription exists but was not completed. Please contact support.", + ), + ExistingSubscriptionAction::PreviousSubscriptionExists { status } => { + MetaHttpResponse::conflict(format!( + "You previously had a subscription (status: {}). Please contact support to reactivate.", + status + )) + } + }; + } + Ok(None) => {} // Good, proceed with creation + Err(e) => { + log::error!("[AWS SAAS] Database error checking customer_id: {}", e); + return MetaHttpResponse::internal_error("Failed to check subscription status"); + } + } + + // 7. Create pending subscription + match aws_mp_db::create_pending_subscription(email, org_id, &resolved).await { + Ok(_) => { + log::info!( + "[AWS SAAS] Created pending subscription: customer_id={}, org_id={}", + resolved.customer_identifier, + org_id + ); + + MetaHttpResponse::json(LinkSubscriptionResponse { + success: true, + customer_identifier: resolved.customer_identifier, + message: None, + }) + } + Err(e) => { + log::error!("[AWS SAAS] Failed to create pending subscription: {}", e); + MetaHttpResponse::internal_error(format!("Failed to create subscription record: {}", e)) + } + } +} + +/// GetAwsMarketplaceActivationStatus +#[utoipa::path( + get, + path="/{org_id}/aws-marketplace/activation-status", + context_path = "/api", + tag = "AWS Marketplace", + operation_id = "GetAwsMarketplaceActivationStatus", + summary = "Check AWS Marketplace subscription activation status", + description = "Poll this endpoint to check if AWS Marketplace subscription is activated. Requires admin role.", + security( + ("Authorization" = []) + ), + params( + ("org_id" = String, Path, description = "Organization identifier"), + ("customer_id" = Option, Query, description = "Optional AWS Marketplace customer identifier for verification"), + ), + responses( + (status = 200, description = "Success", content_type = "application/json", body = Object), + (status = 400, description = "Bad Request (customer_id mismatch)", content_type = "application/json", body = ()), + (status = 403, description = "Forbidden (not admin)", content_type = "application/json", body = ()), + (status = 404, description = "Not Found", content_type = "application/json", body = ()), + (status = 500, description = "Internal Server Error", content_type = "application/json", body = ()), + ), +)] +pub async fn activation_status( + Path(org_id): Path, + Query(query): Query>, + Headers(user_email): Headers, +) -> Response { + let email = user_email.user_id.as_str(); + let org_id = org_id.as_str(); + + log::debug!( + "[AWS SAAS] Checking activation status for org_id={}", + org_id + ); + + // Verify user has access to this org and is an admin + let user = match users::get_user(Some(org_id), email).await { + Some(u) => u, + None => { + return MetaHttpResponse::forbidden( + "User not found or not authorized for this organization", + ); + } + }; + + // Only admins and root users can check subscription status + if user.role != UserRole::Admin && user.role != UserRole::Root { + return MetaHttpResponse::forbidden( + "Only organization administrators can check subscription status", + ); + } + + // Get the subscription for this organization + let billing = match aws_mp_db::get_by_org_id(org_id).await { + Ok(Some(billing)) => billing, + Ok(None) => { + return MetaHttpResponse::not_found( + "No AWS Marketplace subscription found for this organization", + ); + } + Err(e) => { + log::error!("[AWS SAAS] Database error: {}", e); + return MetaHttpResponse::internal_error("Failed to check subscription status"); + } + }; + + // If customer_id is provided in query, verify it matches the org's subscription + if let Some(customer_id) = query.get("customer_id") { + if *customer_id != billing.customer_identifier { + MetaHttpResponse::bad_request( + "The provided customer_id does not match this organization's subscription", + ); + } + } + // If customer_id is not provided, skip verification and proceed (it's optional) + + // Build response based on status + let response = aws_mp_db::build_activation_status_response(&billing); + + MetaHttpResponse::json(response) +} diff --git a/src/handler/http/request/cloud/azure_marketplace.rs b/src/handler/http/request/cloud/azure_marketplace.rs new file mode 100644 index 0000000000..987746da75 --- /dev/null +++ b/src/handler/http/request/cloud/azure_marketplace.rs @@ -0,0 +1,149 @@ +// Copyright 2025 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use axum::{extract::Path, response::Response}; +use config::meta::user::UserRole; +use o2_enterprise::enterprise::cloud::billings; +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{meta::http::HttpResponse as MetaHttpResponse, utils::auth::UserEmail}, + handler::http::extractors::Headers, + service::{organization, users}, +}; + +/// Request payload for linking Azure Marketplace subscription +#[derive(Debug, Deserialize, utoipa::ToSchema)] +pub struct LinkSubscriptionRequest { + pub token: String, +} + +/// Response from link-subscription endpoint +#[derive(Debug, Serialize, utoipa::ToSchema)] +pub struct LinkSubscriptionResponse { + pub success: bool, + pub customer_identifier: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +/// LinkAzureMarketplaceSubscription +#[utoipa::path( + post, + path = "/{org_id}/azure-marketplace/link-subscription", + context_path = "/api", + tag = "Azure Marketplace", + operation_id = "LinkAzureMarketplaceSubscription", + summary = "Link Azure Marketplace subscription to organization", + description = "Processes the token provided by azure and links the subscription to organization", + security( + ("Authorization" = []) + ), + request_body(content = LinkSubscriptionRequest), + responses( + (status = 200, description = "Success", content_type = "application/json", body = LinkSubscriptionResponse), + (status = 400, description = "Bad Request (token expired, invalid)", content_type = "application/json", body = ()), + (status = 403, description = "Forbidden (not admin)", content_type = "application/json", body = ()), + (status = 409, description = "Conflict (already subscribed)", content_type = "application/json", body = ()), + (status = 500, description = "Internal Server Error", content_type = "application/json", body = ()), + ), +)] +pub async fn link_subscription( + Path(org_id): Path, + Headers(user_email): Headers, + axum::Json(req): axum::Json, +) -> Response { + let email = user_email.user_id.as_str(); + let org_id = org_id.as_str(); + + // 1. Validate organization exists + let _org = match organization::get_org(org_id).await { + Some(org) => org, + None => { + return MetaHttpResponse::not_found("Organization not found"); + } + }; + + // 2. Verify user is admin of the organization + let user = match users::get_user(Some(org_id), email).await { + Some(u) => u, + None => { + return MetaHttpResponse::forbidden( + "User not found or not authorized for this organization", + ); + } + }; + + // Only admins and root users can link Azure Marketplace subscriptions + if user.role != UserRole::Admin && user.role != UserRole::Root { + return MetaHttpResponse::forbidden( + "Only organization administrators can link Azure Marketplace subscriptions", + ); + } + + // 3. Check if org is free tier (cannot link paid orgs) + let subscription = match billings::get_billing_by_org_id(org_id).await { + Ok(v) => v, + Err(e) => { + log::error!("[AZURE SAAS] Error checking free trial status: {}", e); + return MetaHttpResponse::internal_error("Failed to check organization status"); + } + }; + + if let Some(sub) = subscription { + if !sub.subscription_type.is_free_sub() { + return MetaHttpResponse::bad_request( + "Only free tier organizations can be linked to Azure Marketplace", + ); + } + } + + let req_id = config::ider::generate_trace_id(); + log::info!( + "[AZURE SAAS] Link subscription request: org_id={}, user={}, req_id={}", + org_id, + email, + req_id + ); + + let res = match billings::azure_utils::process_token(&req.token, &req_id).await { + Ok(r) => r, + Err(e) => { + log::info!("[AZURE SAAS] Error processing token for org {org_id} user {email} : {e}"); + return MetaHttpResponse::bad_request(e); + } + }; + let sub_id = res.id.clone(); + + if let Err(e) = billings::azure_utils::activate_subscription(&res, &req_id).await { + log::error!( + "[AZURE SAAS] Error activating subscription for org {org_id} user {email} sub_id: {sub_id} : {e}", + ); + return MetaHttpResponse::bad_request(e); + } + + if let Err(e) = billings::azure_utils::save_subscription(org_id, email, res, &req_id).await { + log::error!( + "[AZURE SAAS] Error saving subscription for org {org_id} user {email} sub_id: {sub_id} : {e}", + ); + MetaHttpResponse::bad_request(e); + } + + log::info!( + "subscription successfully linked and activated org_id={org_id}, email={email}, req_id={req_id}" + ); + + MetaHttpResponse::created("successfully linked azure subscription") +} diff --git a/src/handler/http/request/cloud/billings.rs b/src/handler/http/request/cloud/billings.rs index eac2cc9689..a3fc549efa 100644 --- a/src/handler/http/request/cloud/billings.rs +++ b/src/handler/http/request/cloud/billings.rs @@ -161,7 +161,6 @@ pub async fn process_session_detail( Some(org) => org, }; - log::debug!("handling checkout session detail"); match o2_cloud_billings::process_checkout_session_details( &org_id, &query.session_id, @@ -169,7 +168,10 @@ pub async fn process_session_detail( ) .await { - Err(e) => e.into_http_response(), + Err(e) => { + log::info!("checkout session errored : {e}"); + e.into_http_response() + } Ok(()) => { let redirect_url = format!( "{}/web/billings/plans?org_identifier={}", @@ -212,9 +214,8 @@ pub async fn process_session_detail( stream_name: None, }) .await; - RedirectResponseBuilder::new(&redirect_url) - .build() - .redirect_http() + let res = RedirectResponseBuilder::new(&redirect_url).build(); + res.redirect_http() } } } @@ -463,3 +464,55 @@ pub async fn handle_stripe_event(headers: HeaderMap, payload: axum::body::Bytes) Err(err) => err.into_http_response(), } } + +/// StripeWebhookEvent +#[utoipa::path( + post, + path="/webhook/azure", + context_path = "/webhook", + tag = "Billings", + summary = "Handle Azure webhook events", + description = "Processes webhook events from Azure for subscription changes and payments", + request_body(content = String, description = "Raw Azure webhook payload"), + responses( + (status = 200, description="Status OK", content_type = "application/json", body = ()) + ) +)] +pub async fn handle_azure_event(headers: HeaderMap, payload: axum::body::Bytes) -> Response { + log::info!("Azure Saas webhook received"); + let Some(auth_header) = headers.get("Authorization").and_then(|v| v.to_str().ok()) else { + return o2_cloud_billings::BillingError::AzureWebhookError("invalid auth header".into()) + .into_http_response(); + }; + + match o2_cloud_billings::azure_utils::handle_azure_wh(auth_header, &payload).await { + Ok(_) => { + log::info!("successfully handled azure saas webhook"); + MetaHttpResponse::json(json::json!({ + "status": "success" + })) + } + Err(e) => { + log::error!("error while handling azure saas webhook : {e}"); + o2_cloud_billings::BillingError::AzureWebhookError("internal error".into()) + .into_http_response() + } + } + + // TODO: send event for this when validated + // for (org_id, sub_type) in orgs { + // let org = match organization::get_org(&org_id).await { + // None => continue, + // Some(org) => org, + // }; + // enqueue_cloud_event(CloudEvent { + // org_id: org.identifier.clone(), + // org_name: org.name.clone(), + // org_type: org.org_type.clone(), + // user: None, + // event: EventType::SubscriptionDeleted, + // subscription_type: Some(sub_type.to_string()), + // stream_name: None, + // }) + // .await; +} diff --git a/src/handler/http/request/cloud/mod.rs b/src/handler/http/request/cloud/mod.rs index 4c63e7a94d..2b17ae9a11 100644 --- a/src/handler/http/request/cloud/mod.rs +++ b/src/handler/http/request/cloud/mod.rs @@ -18,6 +18,8 @@ use o2_enterprise::enterprise::cloud::billings::{self as o2_cloud_billings}; use crate::common::meta::http::HttpResponse as MetaHttpResponse; +pub mod aws_marketplace; +pub mod azure_marketplace; pub mod billings; pub mod marketing; pub mod org_usage; diff --git a/src/handler/http/router/mod.rs b/src/handler/http/router/mod.rs index 1f798762ea..30eb32f1fc 100644 --- a/src/handler/http/router/mod.rs +++ b/src/handler/http/router/mod.rs @@ -390,7 +390,17 @@ pub fn basic_routes() -> Router { { router = router.nest( "/webhook", - Router::new().route("/stripe", post(cloud::billings::handle_stripe_event)), + Router::new() + .route("/stripe", post(cloud::billings::handle_stripe_event)) + .route("/azure", post(cloud::billings::handle_azure_event)), + ); + + // AWS Marketplace registration endpoint - receives POST from AWS Marketplace + // Must be publicly accessible (no auth) as users haven't logged in yet + // Using /marketplace path to avoid conflict with authenticated /api scope + router = router.route( + "/marketplace/aws/register", + post(cloud::aws_marketplace::aws_marketplace_register), ); } @@ -823,6 +833,18 @@ pub fn service_routes() -> Router { .route( "/{org_id}/extend_trial_period", put(organization::org::extend_trial_period), + ) + .route( + "/{org_id}/aws-marketplace/link-subscription", + post(cloud::aws_marketplace::link_subscription), + ) + .route( + "/{org_id}/aws-marketplace/activation-status", + get(cloud::aws_marketplace::activation_status), + ) + .route( + "/{org_id}/azure-marketplace/link-subscription", + post(cloud::azure_marketplace::link_subscription), ); } diff --git a/src/job/mod.rs b/src/job/mod.rs index ef6837d6a1..2d6ca09e74 100644 --- a/src/job/mod.rs +++ b/src/job/mod.rs @@ -80,6 +80,62 @@ async fn enforce_usage_stream_retention() { } } +#[cfg(feature = "cloud")] +async fn get_metering_lock() -> Result, infra::errors::Error> { + if !LOCAL_NODE.is_alert_manager() { + return Ok(None); + } + use infra::{cluster::get_node_by_uuid, dist_lock}; + + let db = infra::db::get_db().await; + let node = db + .get("/cloud/metering/node") + .await + .ok() + .unwrap_or_default(); + let node = String::from_utf8_lossy(&node); + if !node.is_empty() && LOCAL_NODE.uuid.ne(&node) && get_node_by_uuid(&node).await.is_some() { + log::info!("[o2::ENT] metering is locked by node {node}"); + return Ok(None); // other node is processing + } + + if node.is_empty() || LOCAL_NODE.uuid.ne(&node) { + let locker = infra::dist_lock::lock("/cloud/metering/node", 0).await?; + // check the working node again, maybe other node locked it first + let node = db + .get("/cloud/metering/node") + .await + .ok() + .unwrap_or_default(); + let node = String::from_utf8_lossy(&node); + if !node.is_empty() && LOCAL_NODE.uuid.ne(&node) && get_node_by_uuid(&node).await.is_some() + { + dist_lock::unlock(&locker).await?; + return Ok(None); // other node is processing + } + // set to current node + let ret = db + .put( + "/cloud/metering/node", + LOCAL_NODE.uuid.clone().into(), + infra::db::NO_NEED_WATCH, + None, + ) + .await; + // Check db.put result before releasing the lock to ensure consistent state + if let Err(e) = ret { + dist_lock::unlock(&locker).await?; + drop(locker); + return Err(e); + } + dist_lock::unlock(&locker).await?; + log::info!("[o2::ENT] Metering lock acquired"); + drop(locker); + } + + Ok(Some(())) +} + pub async fn init() -> Result<(), anyhow::Error> { let email_regex = Regex::new( r"^([a-z0-9_+]([a-z0-9_+.-]*[a-z0-9_+])?)@([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6})", @@ -393,9 +449,18 @@ pub async fn init() -> Result<(), anyhow::Error> { .expect("cloud ofga migrations failed"); use crate::service::self_reporting::{ingest_data_retention_usages, search::get_usage}; - o2_enterprise::enterprise::metering::init(get_usage, ingest_data_retention_usages) + o2_enterprise::enterprise::metering::init( + get_metering_lock, + get_usage, + ingest_data_retention_usages, + ) + .await + .expect("cloud usage metering job init failed"); + + // Initialize AWS Marketplace SQS notification polling + o2_enterprise::enterprise::aws_marketplace::init() .await - .expect("cloud usage metering job init failed"); + .expect("AWS Marketplace integration init failed"); // run these cloud jobs only in alert manager if LOCAL_NODE.is_alert_manager() { diff --git a/src/service/alerts/scheduler/handlers.rs b/src/service/alerts/scheduler/handlers.rs index 87c43ab36c..d05b45087c 100644 --- a/src/service/alerts/scheduler/handlers.rs +++ b/src/service/alerts/scheduler/handlers.rs @@ -346,6 +346,21 @@ async fn handle_alert_triggers( &trigger.module_key ); } + + if let Err(e) = db::scheduler::delete( + &trigger.org, + db::scheduler::TriggerModule::Alert, + &trigger.module_key, + ) + .await + { + log::error!( + "[SCHEDULER trace_id {scheduler_trace_id}] Failed to remove alert from scheduled_jobs due to trial expiry: {}/{} : {e}", + &trigger.org, + &trigger.module_key + ); + } + return Ok(()); } } @@ -1213,6 +1228,21 @@ async fn handle_report_triggers( report_name ); } + + if let Err(e) = db::scheduler::delete( + &trigger.org, + db::scheduler::TriggerModule::Report, + &trigger.module_key, + ) + .await + { + log::error!( + "[SCHEDULER trace_id {scheduler_trace_id}] Failed to remove report from scheduled_jobs due to trial expiry: {}/{} : {e}", + &trigger.org, + report_name + ); + } + return Ok(()); } } @@ -1518,6 +1548,21 @@ async fn handle_derived_stream_triggers( pipeline.name ); } + + if let Err(e) = db::scheduler::delete( + &trigger.org, + db::scheduler::TriggerModule::DerivedStream, + &trigger.module_key, + ) + .await + { + log::error!( + "[SCHEDULER trace_id {scheduler_trace_id}] Failed to remove pipeline from scheduled_jobs due to trial expiry: {}/{} : {e}", + &trigger.org, + pipeline.name + ); + } + return Ok(()); } } diff --git a/src/service/organization.rs b/src/service/organization.rs index c107fed5d2..2b9c3b1998 100644 --- a/src/service/organization.rs +++ b/src/service/organization.rs @@ -343,15 +343,23 @@ pub async fn create_org( #[cfg(feature = "cloud")] { + let o2cfg = o2_enterprise::enterprise::common::config::get_config(); let orgs = list_orgs_by_user(user_email).await?; + let mut free_org_count = 0; for org in orgs { - let billing = get_billing_by_org_id(&org.identifier).await?; - if billing.is_none() { - return Err(anyhow::anyhow!( - "A user cannot be part of multiple free accounts" - )); + let billings = get_billing_by_org_id(&org.identifier).await?; + if billings.is_none() { + free_org_count += 1; } } + // if we allow creating a new org, the free org count would be max+1 + // hence check of >= rather than > + if free_org_count >= o2cfg.cloud.max_free_orgs_allowed { + return Err(anyhow::anyhow!( + "A user cannot be part of more than {} free organizations.", + o2cfg.cloud.max_free_orgs_allowed + )); + } } org.name = org.name.trim().to_owned(); @@ -735,14 +743,23 @@ pub async fn generate_invitation( // irrespective of what other orgs invitees are part of. // if it is a free org, we must check that the orgs invitee is already part // of are all paid, as one user cannot be part of more than one free org. + let o2cfg = o2_enterprise::enterprise::common::config::get_config(); + let mut free_org_count = 0; let invitee_orgs = list_orgs_by_user(invitee).await?; for org in invitee_orgs { - if get_billing_by_org_id(&org.identifier).await?.is_none() { - return Err(anyhow::anyhow!( - "Invitee {invitee} is already part of another free org, cannot be invited in this org" - )); + let billings = get_billing_by_org_id(&org.identifier).await?; + if billings.is_none() { + free_org_count += 1; } } + // if we allow the invite the potential count would be max+1 (after accepting) + // hence >= instead of > + if free_org_count >= o2cfg.cloud.max_free_orgs_allowed { + return Err(anyhow::anyhow!( + "Invitee {invitee} is already part of {} free organizations, cannot be invited in this organization.", + o2cfg.cloud.max_free_orgs_allowed + )); + } } } if let Some(org) = get_org(org_id).await { @@ -823,14 +840,24 @@ pub async fn accept_invitation(user_email: &str, invite_token: &str) -> Result<( // if the org user is joining is paid, no issues, we can just let them join // if it is a free org, we must check that the orgs the joining user is already part // of are all paid, as one user cannot be part of more than one free org. + let o2cfg = o2_enterprise::enterprise::common::config::get_config(); + let mut free_org_count = 0; let user_orgs = list_orgs_by_user(user_email).await?; for org in user_orgs { - if get_billing_by_org_id(&org.identifier).await?.is_none() { - return Err(anyhow::anyhow!( - "User is already a part of a free organization. A user cannot join multiple free orgs." - )); + let billings = get_billing_by_org_id(&org.identifier).await?; + if billings.is_none() { + free_org_count += 1; } } + // if we allow the invite the potential count would be max+1 (after accepting) + // hence >= instead of > + if free_org_count >= o2cfg.cloud.max_free_orgs_allowed { + return Err(anyhow::anyhow!( + "User is already a part of {} free organization. A user cannot join more than {} free organizations.", + o2cfg.cloud.max_free_orgs_allowed, + o2cfg.cloud.max_free_orgs_allowed + )); + } } // Check if user is already part of the org diff --git a/web/src/enterprise/components/billings/Billing.spec.ts b/web/src/enterprise/components/billings/Billing.spec.ts index c4d6d62ad2..4a4b683caf 100644 --- a/web/src/enterprise/components/billings/Billing.spec.ts +++ b/web/src/enterprise/components/billings/Billing.spec.ts @@ -49,13 +49,28 @@ vi.mock("quasar", async (importOriginal) => { }; }); +// Mock billing service - using factory function to avoid hoisting issues +vi.mock("@/services/billings", () => ({ + default: { + list_subscription: vi.fn() + } +})); + +// Import after mocking +import BillingService from "@/services/billings"; + describe("Billing Component", () => { let wrapper: any = null; - beforeEach(() => { + beforeEach(async () => { // Reset mocks vi.clearAllMocks(); - + + // Mock billing service response + (BillingService.list_subscription as any).mockResolvedValue({ + data: { provider: "stripe" } + }); + // Reset router state mockRouter.currentRoute.value.name = "billings"; mockRouter.currentRoute.value.query = { @@ -90,6 +105,9 @@ describe("Billing Component", () => { } }, }); + + // Wait for onMounted to complete + await wrapper.vm.$nextTick(); }); afterEach(() => { @@ -108,7 +126,7 @@ describe("Billing Component", () => { }); it("should initialize with correct data", () => { - expect(wrapper.vm.billingtab).toBe("plans"); // Should be set to "plans" in onMounted + expect(wrapper.vm.billingtab).toBe("plans"); // Should be set to "plans" in onMounted after fetchBillingInfo expect(wrapper.vm.usageDataType).toBe("gb"); expect(wrapper.vm.splitterModel).toBe(220); expect(wrapper.vm.usageDate).toBe("30days"); @@ -144,7 +162,8 @@ describe("Billing Component", () => { }); describe("onMounted Lifecycle Hook", () => { - it("should redirect to plans when route is billings", () => { + it("should redirect to plans when route is billings", async () => { + // The wrapper was already mounted in beforeEach and onMounted completed expect(mockRouter.push).toHaveBeenCalledWith({ path: "/billings/plans", query: { org_identifier: store.state.selectedOrganization.identifier } @@ -152,10 +171,19 @@ describe("Billing Component", () => { expect(wrapper.vm.billingtab).toBe("plans"); }); - it("should redirect to plans when route is plans", () => { + it("should redirect to plans when route is plans", async () => { + // Reset router state and clear mocks before mounting mockRouter.currentRoute.value.name = "plans"; - mockRouter.push.mockClear(); - + mockRouter.currentRoute.value.query = { + data_type: "gb", + usage_date: "30days" + }; + + // Mock billing service response for this test + (BillingService.list_subscription as any).mockResolvedValue({ + data: { provider: "stripe" } + }); + const testWrapper = mount(Billing, { global: { plugins: [i18n], @@ -181,7 +209,12 @@ describe("Billing Component", () => { } }, }); - + + // Wait for onMounted to complete (including fetchBillingInfo) + await testWrapper.vm.$nextTick(); + await testWrapper.vm.$nextTick(); + + // The router.push should have been called during onMounted expect(mockRouter.push).toHaveBeenCalledWith({ path: "/billings/plans", query: { org_identifier: store.state.selectedOrganization.identifier } @@ -190,10 +223,10 @@ describe("Billing Component", () => { testWrapper.unmount(); }); - it("should not redirect when route is usage", () => { + it("should not redirect when route is usage", async () => { mockRouter.currentRoute.value.name = "usage"; mockRouter.push.mockClear(); - + const testWrapper = mount(Billing, { global: { plugins: [i18n], @@ -219,7 +252,10 @@ describe("Billing Component", () => { } }, }); - + + // Wait for onMounted to complete + await testWrapper.vm.$nextTick(); + expect(mockRouter.push).not.toHaveBeenCalled(); testWrapper.unmount(); }); @@ -445,9 +481,9 @@ describe("Billing Component", () => { }); describe("Query Parameter Initialization", () => { - it("should initialize usageDataType from router query", () => { + it("should initialize usageDataType from router query", async () => { mockRouter.currentRoute.value.query.data_type = "mb"; - + const testWrapper = mount(Billing, { global: { plugins: [i18n], @@ -473,14 +509,16 @@ describe("Billing Component", () => { } }, }); - + + await testWrapper.vm.$nextTick(); + expect(testWrapper.vm.usageDataType).toBe("mb"); testWrapper.unmount(); }); - it("should initialize usageDate from router query", () => { + it("should initialize usageDate from router query", async () => { mockRouter.currentRoute.value.query.usage_date = "3months"; - + const testWrapper = mount(Billing, { global: { plugins: [i18n], @@ -506,14 +544,16 @@ describe("Billing Component", () => { } }, }); - + + await testWrapper.vm.$nextTick(); + expect(testWrapper.vm.usageDate).toBe("3months"); testWrapper.unmount(); }); - it("should use default values when query parameters are missing", () => { + it("should use default values when query parameters are missing", async () => { mockRouter.currentRoute.value.query = {}; - + const testWrapper = mount(Billing, { global: { plugins: [i18n], @@ -539,7 +579,9 @@ describe("Billing Component", () => { } }, }); - + + await testWrapper.vm.$nextTick(); + expect(testWrapper.vm.usageDataType).toBe("gb"); expect(testWrapper.vm.usageDate).toBe("30days"); testWrapper.unmount(); diff --git a/web/src/enterprise/components/billings/Billing.vue b/web/src/enterprise/components/billings/Billing.vue index 99c184f8a9..88eed2fa67 100644 --- a/web/src/enterprise/components/billings/Billing.vue +++ b/web/src/enterprise/components/billings/Billing.vue @@ -62,45 +62,46 @@ along with this program. If not, see . vertical > - - - - + + + + +
+ +
+ Billing is handled through your AWS account +
+
+
+ +
+ Billing is handled through your Azure account +
+
+ { - const parentRoutes: any = []; + // Note: AWS Marketplace registration is handled by backend at POST /api/aws-marketplace/register + // The backend sets a cookie and redirects to Dex login + const parentRoutes: any = [ + { + // Post-login setup page for org selection/creation + path: "/marketplace/aws/setup", + name: "awsMarketplaceSetup", + component: AwsMarketplaceSetup, + meta: { + title: "AWS Marketplace Setup", + requiresAuth: true, + }, + }, + { + // Entry point from Azure Marketplace - saves token and redirects to login + path: "/marketplace/azure/register", + name: "azureMarketplaceRegister", + component: AzureMarketplaceSetup, + beforeEnter: (to: any, from: any, next: any) => { + const token = to.query.token; + if (token) { + // Save token for after login - Login.vue will check this + sessionStorage.setItem("azure_marketplace_token", token); + } + next(); + }, + }, + ]; const homeChildRoutes = [ { diff --git a/web/src/locales/languages/de.json b/web/src/locales/languages/de.json index a6bd630534..37e819c246 100644 --- a/web/src/locales/languages/de.json +++ b/web/src/locales/languages/de.json @@ -2684,5 +2684,40 @@ "clearChatTitle": "Chat löschen", "clearChatMessage": "Bist du sicher, dass du das Gespräch beenden willst?", "copiedToClipboard": "In die Zwischenablage kopiert" + }, + "awsMarketplace": { + "title": "AWS Marketplace-Registrierung", + "subtitle": "Vervollständigen Sie die Einrichtung Ihres OpenObserve-Abonnements", + "invalidLink": "Ungültiger Registrierungslink", + "invalidLinkDescription": "Dieser Registrierungslink ist ungültig oder abgelaufen. Bitte abonnieren Sie erneut im AWS Marketplace.", + "goToMarketplace": "Gehen Sie zum AWS Marketplace", + "newUser": "Neu bei OpenObserve?", + "newUserDescription": "Neues Konto und Organisation erstellen", + "createAccount": "Konto erstellen", + "existingUser": "Hast du schon ein Konto?", + "existingUserDescription": "Melden Sie sich an, um eine neue Organisation mit AWS-Abrechnung zu erstellen", + "login": "Einloggen", + "loggedInAs": "Eingeloggt als", + "createNewOrg": "Neue Organisation erstellen", + "createNewOrgDescription": "Erstellen Sie eine neue Organisation mit AWS Marketplace-Abrechnung", + "orgName": "Name der Organisation", + "createOrganization": "Organisation erstellen", + "linkToExisting": "Link zur bestehenden Organisation", + "linkToExistingDescription": "Verknüpfen Sie die AWS-Abrechnung mit einer Ihrer Organisationen mit kostenlosem Nutzungskontingent", + "selectOrganization": "Wählen Sie Organisation", + "linkAwsBilling": "AWS-Abrechnung verknüpfen", + "settingUp": "Ihr Abonnement wird eingerichtet...", + "pleaseWait": "Bitte warten Sie, bis wir Ihr Konto konfiguriert haben.", + "waitingConfirmation": "Warten auf die AWS-Bestätigung", + "waitingDescription": "Ihr Abonnement wird aktiviert. Dies dauert normalerweise weniger als eine Minute.", + "customerId": "Kunden-ID", + "subscriptionActivated": "Abonnement aktiviert!", + "activatedDescription": "Ihr AWS Marketplace-Abonnement ist jetzt aktiv.", + "goToDashboard": "Gehe zum Dashboard", + "paymentFailed": "Zahlung fehlgeschlagen", + "paymentFailedDescription": "Es gab ein Problem mit Ihrer AWS Marketplace-Zahlung. Bitte überprüfen Sie Ihr AWS-Konto oder wenden Sie sich an den AWS-Support.", + "contactSupport": "Support kontaktieren", + "tryAgain": "Versuchen Sie es erneut", + "activationTimeout": "Aktivierungs-Timeout. Bitte wenden Sie sich an den Support, falls das Problem weiterhin besteht." } } \ No newline at end of file diff --git a/web/src/locales/languages/en.json b/web/src/locales/languages/en.json index 4220b29c6c..7a836f732b 100644 --- a/web/src/locales/languages/en.json +++ b/web/src/locales/languages/en.json @@ -1736,6 +1736,41 @@ "contactSupport": "Contact Support", "totalUsage": "Total Usage" }, + "awsMarketplace": { + "title": "AWS Marketplace Registration", + "subtitle": "Complete your OpenObserve subscription setup", + "invalidLink": "Invalid Registration Link", + "invalidLinkDescription": "This registration link is invalid or has expired. Please subscribe again from AWS Marketplace.", + "goToMarketplace": "Go to AWS Marketplace", + "newUser": "New to OpenObserve?", + "newUserDescription": "Create a new account and organization", + "createAccount": "Create Account", + "existingUser": "Already have an account?", + "existingUserDescription": "Login to create a new organization with AWS billing", + "login": "Login", + "loggedInAs": "Logged in as", + "createNewOrg": "Create New Organization", + "createNewOrgDescription": "Create a new organization with AWS Marketplace billing", + "orgName": "Organization Name", + "createOrganization": "Create Organization", + "linkToExisting": "Link to Existing Organization", + "linkToExistingDescription": "Link AWS billing to one of your free-tier organizations", + "selectOrganization": "Select Organization", + "linkAwsBilling": "Link AWS Billing", + "settingUp": "Setting up your subscription...", + "pleaseWait": "Please wait while we configure your account.", + "waitingConfirmation": "Waiting for AWS Confirmation", + "waitingDescription": "Your subscription is being activated. This usually takes less than a minute.", + "customerId": "Customer ID", + "subscriptionActivated": "Subscription Activated!", + "activatedDescription": "Your AWS Marketplace subscription is now active.", + "goToDashboard": "Go to Dashboard", + "paymentFailed": "Payment Failed", + "paymentFailedDescription": "There was an issue with your AWS Marketplace payment. Please check your AWS account or contact AWS support.", + "contactSupport": "Contact Support", + "tryAgain": "Try Again", + "activationTimeout": "Activation timeout. Please contact support if the issue persists." + }, "message": { "ServerInternalError": "Error: Something went wrong. Internal server error.", "SearchSQLNotValid": "Error: Invalid SQL.", diff --git a/web/src/locales/languages/es.json b/web/src/locales/languages/es.json index 47d0adb64d..9f769b99e7 100644 --- a/web/src/locales/languages/es.json +++ b/web/src/locales/languages/es.json @@ -2684,5 +2684,40 @@ "clearChatTitle": "Borrar chat", "clearChatMessage": "¿Estás seguro de que quieres borrar esta conversación?", "copiedToClipboard": "Copiado al portapapeles" + }, + "awsMarketplace": { + "title": "Registro en AWS Marketplace", + "subtitle": "Complete la configuración de su suscripción a OpenObserve", + "invalidLink": "Enlace de registro no válido", + "invalidLinkDescription": "Este enlace de registro no es válido o ha caducado. Vuelva a suscribirse desde AWS Marketplace.", + "goToMarketplace": "Ir a AWS Marketplace", + "newUser": "¿Es nuevo en OpenObserve?", + "newUserDescription": "Crear una nueva cuenta y organización", + "createAccount": "Crear cuenta", + "existingUser": "¿Ya tienes una cuenta?", + "existingUserDescription": "Inicie sesión para crear una nueva organización con AWS Billing", + "login": "Iniciar sesión", + "loggedInAs": "Ha iniciado sesión como", + "createNewOrg": "Crear nueva organización", + "createNewOrgDescription": "Cree una nueva organización con la facturación de AWS Marketplace", + "orgName": "Nombre de la organización", + "createOrganization": "Crear organización", + "linkToExisting": "Enlace a la organización existente", + "linkToExistingDescription": "Vincula la facturación de AWS a una de tus organizaciones de nivel gratuito", + "selectOrganization": "Seleccione la organización", + "linkAwsBilling": "Vincular AWS Billing", + "settingUp": "Configurando tu suscripción...", + "pleaseWait": "Espere mientras configuramos su cuenta.", + "waitingConfirmation": "Esperando la confirmación de AWS", + "waitingDescription": "Se está activando tu suscripción. Esto suele tardar menos de un minuto.", + "customerId": "ID de cliente", + "subscriptionActivated": "¡Suscripción activada!", + "activatedDescription": "Su suscripción a AWS Marketplace ya está activa.", + "goToDashboard": "Ir al panel", + "paymentFailed": "Fallo en el pago", + "paymentFailedDescription": "Se ha producido un problema con el pago de AWS Marketplace. Compruebe su cuenta de AWS o póngase en contacto con el servicio de asistencia de AWS.", + "contactSupport": "Comuníquese con el soporte", + "tryAgain": "Inténtalo de nuevo", + "activationTimeout": "Tiempo de espera de activación. Ponte en contacto con el servicio de asistencia si el problema persiste." } } \ No newline at end of file diff --git a/web/src/locales/languages/fr.json b/web/src/locales/languages/fr.json index 9ace6f4a44..c6ab72e8db 100644 --- a/web/src/locales/languages/fr.json +++ b/web/src/locales/languages/fr.json @@ -2684,5 +2684,40 @@ "clearChatTitle": "Effacer le chat", "clearChatMessage": "Êtes-vous sûr de vouloir effacer cette conversation ?", "copiedToClipboard": "Copié dans le presse-papiers" + }, + "awsMarketplace": { + "title": "Inscription à AWS Marketplace", + "subtitle": "Terminez la configuration de votre abonnement OpenObserve", + "invalidLink": "Lien d'inscription non valide", + "invalidLinkDescription": "Ce lien d'inscription n'est pas valide ou a expiré. Veuillez vous inscrire à nouveau sur AWS Marketplace.", + "goToMarketplace": "Accédez à AWS Marketplace", + "newUser": "Vous êtes nouveau sur OpenObserve ?", + "newUserDescription": "Créer un nouveau compte et une nouvelle organisation", + "createAccount": "Créer un compte", + "existingUser": "Vous avez déjà un compte ?", + "existingUserDescription": "Connectez-vous pour créer une nouvelle organisation avec AWS Billing", + "login": "Se connecter", + "loggedInAs": "Connecté en tant que", + "createNewOrg": "Créer une nouvelle organisation", + "createNewOrgDescription": "Créez une nouvelle organisation grâce à la facturation AWS Marketplace", + "orgName": "Nom de l'organisation", + "createOrganization": "Créer une organisation", + "linkToExisting": "Lien vers l'organisation existante", + "linkToExistingDescription": "Associez la facturation AWS à l'une de vos organisations de niveau gratuit", + "selectOrganization": "Sélectionnez une organisation", + "linkAwsBilling": "Lien vers AWS Billing", + "settingUp": "Configuration de votre abonnement...", + "pleaseWait": "Veuillez patienter pendant que nous configurons votre compte.", + "waitingConfirmation": "En attente de confirmation par AWS", + "waitingDescription": "Votre abonnement est en cours d'activation. Cela prend généralement moins d'une minute.", + "customerId": "ID client", + "subscriptionActivated": "Abonnement activé !", + "activatedDescription": "Votre abonnement AWS Marketplace est désormais actif.", + "goToDashboard": "Accédez au tableau de bord", + "paymentFailed": "Echec du paiement", + "paymentFailedDescription": "Un problème est survenu lors de votre paiement sur AWS Marketplace. Vérifiez votre compte AWS ou contactez le support AWS.", + "contactSupport": "Contacter le service d'assistance", + "tryAgain": "Essayez à nouveau", + "activationTimeout": "Délai d'activation. Veuillez contacter le support si le problème persiste." } } \ No newline at end of file diff --git a/web/src/locales/languages/it.json b/web/src/locales/languages/it.json index ccc7692644..e02dc394e0 100644 --- a/web/src/locales/languages/it.json +++ b/web/src/locales/languages/it.json @@ -2684,5 +2684,40 @@ "clearChatTitle": "Cancella chat", "clearChatMessage": "Sei sicuro di voler chiarire questa conversazione?", "copiedToClipboard": "Copiato negli appunti" + }, + "awsMarketplace": { + "title": "Registrazione AWS Marketplace", + "subtitle": "Completa la configurazione dell'abbonamento OpenObserve", + "invalidLink": "Link di registrazione non valido", + "invalidLinkDescription": "Questo link di registrazione non è valido o è scaduto. Abbonati nuovamente da AWS Marketplace.", + "goToMarketplace": "Vai ad AWS Marketplace", + "newUser": "Sei nuovo su OpenObserve?", + "newUserDescription": "Crea un nuovo account e una nuova organizzazione", + "createAccount": "Crea un account", + "existingUser": "Hai già un account?", + "existingUserDescription": "Accedi per creare una nuova organizzazione con fatturazione AWS", + "login": "Accedi", + "loggedInAs": "Effettuato l'accesso come", + "createNewOrg": "Crea nuova organizzazione", + "createNewOrgDescription": "Crea una nuova organizzazione con la fatturazione di AWS Marketplace", + "orgName": "Nome dell'organizzazione", + "createOrganization": "Crea organizzazione", + "linkToExisting": "Collegamento a un'organizzazione esistente", + "linkToExistingDescription": "Collega la fatturazione AWS a una delle tue organizzazioni di livello gratuito", + "selectOrganization": "Seleziona organizzazione", + "linkAwsBilling": "Collega la fatturazione AWS", + "settingUp": "Configurazione dell'abbonamento...", + "pleaseWait": "Attendi mentre configuriamo il tuo account.", + "waitingConfirmation": "In attesa della conferma di AWS", + "waitingDescription": "L'abbonamento è in fase di attivazione. Di solito ci vuole meno di un minuto.", + "customerId": "ID cliente", + "subscriptionActivated": "Abbonamento attivato!", + "activatedDescription": "Il tuo abbonamento ad AWS Marketplace è ora attivo.", + "goToDashboard": "Vai alla dashboard", + "paymentFailed": "Pagamento non riuscito", + "paymentFailedDescription": "Si è verificato un problema con il pagamento di AWS Marketplace. Controlla il tuo account AWS o contatta l'assistenza AWS.", + "contactSupport": "Contatta l'assistenza", + "tryAgain": "Riprova", + "activationTimeout": "Timeout di attivazione. Contatta l'assistenza se il problema persiste." } } \ No newline at end of file diff --git a/web/src/locales/languages/ja.json b/web/src/locales/languages/ja.json index cddea1b7e0..a7e911618d 100644 --- a/web/src/locales/languages/ja.json +++ b/web/src/locales/languages/ja.json @@ -2677,5 +2677,40 @@ "clearChatTitle": "チャットをクリア", "clearChatMessage": "この会話をクリアしてもよろしいですか?", "copiedToClipboard": "クリップボードにコピー" + }, + "awsMarketplace": { + "title": "AWS マーケットプレイス登録", + "subtitle": "OpenObserveサブスクリプションのセットアップを完了してください", + "invalidLink": "登録リンクが無効です", + "invalidLinkDescription": "この登録リンクは無効か、有効期限が切れています。AWS Marketplace から再度購読してください。", + "goToMarketplace": "AWS マーケットプレイスに移動", + "newUser": "オープンオブザーブは初めてですか?", + "newUserDescription": "新しいアカウントと組織を作成する", + "createAccount": "アカウント作成", + "existingUser": "すでにアカウントをお持ちですか?", + "existingUserDescription": "ログインして AWS 請求で新しい組織を作成する", + "login": "ログイン", + "loggedInAs": "としてログイン", + "createNewOrg": "新しい組織を作成", + "createNewOrgDescription": "AWS Marketplace 請求を利用して新しい組織を作成する", + "orgName": "[組織名]", + "createOrganization": "組織を作成", + "linkToExisting": "既存の組織へのリンク", + "linkToExistingDescription": "AWS 請求をいずれかの無料利用枠組織にリンクする", + "selectOrganization": "組織を選択", + "linkAwsBilling": "AWS 請求のリンク", + "settingUp": "サブスクリプションをセットアップ中...", + "pleaseWait": "アカウントを設定中です。しばらくお待ちください。", + "waitingConfirmation": "AWS の確認を待っています", + "waitingDescription": "サブスクリプションはアクティベートされています。通常、この処理には 1 分もかかりません。", + "customerId": "カスタマー ID", + "subscriptionActivated": "購読がアクティベートされました!", + "activatedDescription": "お客様の AWS Marketplace サブスクリプションがアクティブになりました。", + "goToDashboard": "ダッシュボードに移動", + "paymentFailed": "支払い失敗", + "paymentFailedDescription": "お客様の AWS マーケットプレイスの支払いに問題がありました。AWS アカウントを確認するか、AWS サポートに連絡してください。", + "contactSupport": "サポートに連絡", + "tryAgain": "もう一度試す", + "activationTimeout": "アクティベーションタイムアウト。問題が解決しない場合は、サポートに連絡してください。" } } \ No newline at end of file diff --git a/web/src/locales/languages/ko.json b/web/src/locales/languages/ko.json index d57926766a..c8486fae2b 100644 --- a/web/src/locales/languages/ko.json +++ b/web/src/locales/languages/ko.json @@ -2684,5 +2684,40 @@ "clearChatTitle": "채팅 지우기", "clearChatMessage": "정말 이 대화를 지우고 싶으신가요?", "copiedToClipboard": "클립보드에 복사됨" + }, + "awsMarketplace": { + "title": "AWS 마켓플레이스 등록", + "subtitle": "오픈옵저브 서브스크립션 설정을 완료하세요", + "invalidLink": "잘못된 등록 링크", + "invalidLinkDescription": "이 등록 링크가 유효하지 않거나 만료되었습니다.AWS 마켓플레이스에서 다시 구독하십시오.", + "goToMarketplace": "AWS 마켓플레이스로 이동", + "newUser": "오픈옵저브를 처음 사용하시나요?", + "newUserDescription": "새 계정 및 조직 만들기", + "createAccount": "계정 생성", + "existingUser": "이미 계정이 있으신가요?", + "existingUserDescription": "로그인하여 AWS 결제로 새 조직을 생성하십시오.", + "login": "로그인", + "loggedInAs": "로 로그인", + "createNewOrg": "새 조직 생성", + "createNewOrgDescription": "AWS 마켓플레이스 결제로 새 조직 만들기", + "orgName": "조직 이름", + "createOrganization": "조직 생성", + "linkToExisting": "기존 조직에 연결", + "linkToExistingDescription": "AWS 결제를 프리 티어 조직 중 하나에 연결", + "selectOrganization": "조직 선택", + "linkAwsBilling": "AWS 결제 연결", + "settingUp": "구독 설정 중...", + "pleaseWait": "계정을 구성하는 동안 잠시 기다려 주십시오.", + "waitingConfirmation": "AWS 확인을 기다리는 중", + "waitingDescription": "구독이 활성화되고 있습니다.보통 1분도 채 걸리지 않습니다.", + "customerId": "고객 ID", + "subscriptionActivated": "구독이 활성화되었습니다!", + "activatedDescription": "이제 AWS 마켓플레이스 구독이 활성화되었습니다.", + "goToDashboard": "대시보드로 이동", + "paymentFailed": "결제 실패", + "paymentFailedDescription": "AWS 마켓플레이스 결제에 문제가 발생했습니다.AWS 계정을 확인하거나 AWS 지원에 문의하십시오.", + "contactSupport": "지원팀에 문의", + "tryAgain": "다시 시도하세요", + "activationTimeout": "활성화 제한 시간이 초과되었습니다.문제가 지속되면 지원팀에 문의하세요." } } \ No newline at end of file diff --git a/web/src/locales/languages/nl.json b/web/src/locales/languages/nl.json index b7906756bc..2848cce98d 100644 --- a/web/src/locales/languages/nl.json +++ b/web/src/locales/languages/nl.json @@ -2684,5 +2684,40 @@ "clearChatTitle": "Chat wissen", "clearChatMessage": "Weet je zeker dat je dit gesprek wilt beëindigen?", "copiedToClipboard": "Gekopieerd naar klembord" + }, + "awsMarketplace": { + "title": "AWS Marketplace-registratie", + "subtitle": "Voltooi de configuratie van uw OpenObserve-abonnement", + "invalidLink": "Ongeldige registratielink", + "invalidLinkDescription": "Deze registratielink is ongeldig of is verlopen. Meld u opnieuw aan via AWS Marketplace.", + "goToMarketplace": "Ga naar AWS Marketplace", + "newUser": "Ben je nieuw bij OpenObserve?", + "newUserDescription": "Maak een nieuw account en nieuwe organisatie", + "createAccount": "Account aanmaken", + "existingUser": "Heb je al een account?", + "existingUserDescription": "Log in om een nieuwe organisatie te creëren met AWS-facturering", + "login": "Login", + "loggedInAs": "Ingelogd als", + "createNewOrg": "Creëer een nieuwe organisatie", + "createNewOrgDescription": "Creëer een nieuwe organisatie met AWS Marketplace-facturering", + "orgName": "Naam van de organisatie", + "createOrganization": "Creëer een organisatie", + "linkToExisting": "Link naar bestaande organisatie", + "linkToExistingDescription": "Koppel AWS-facturering aan een van uw gratis organisaties", + "selectOrganization": "Organisatie selecteren", + "linkAwsBilling": "AWS-facturering koppelen", + "settingUp": "Je abonnement instellen...", + "pleaseWait": "Een ogenblik geduld a.u.b. terwijl we uw account configureren.", + "waitingConfirmation": "Wachten op bevestiging van AWS", + "waitingDescription": "Je abonnement wordt geactiveerd. Dit duurt meestal minder dan een minuut.", + "customerId": "Klant-ID", + "subscriptionActivated": "Abonnement geactiveerd!", + "activatedDescription": "Je AWS Marketplace-abonnement is nu actief.", + "goToDashboard": "Ga naar het dashboard", + "paymentFailed": "Betaling is mislukt", + "paymentFailedDescription": "Er was een probleem met je AWS Marketplace-betaling. Controleer je AWS-account of neem contact op met AWS-ondersteuning.", + "contactSupport": "Contact opnemen met de klantenservice", + "tryAgain": "Probeer het opnieuw", + "activationTimeout": "Time-out voor activering. Neem contact op met de klantenservice als het probleem zich blijft voordoen." } } \ No newline at end of file diff --git a/web/src/locales/languages/pt.json b/web/src/locales/languages/pt.json index 214d5413ad..5b6698b1ee 100644 --- a/web/src/locales/languages/pt.json +++ b/web/src/locales/languages/pt.json @@ -2684,5 +2684,40 @@ "clearChatTitle": "Limpar bate-papo", "clearChatMessage": "Tem certeza de que deseja encerrar essa conversa?", "copiedToClipboard": "Copiado para a prancheta" + }, + "awsMarketplace": { + "title": "Registro no AWS Marketplace", + "subtitle": "Conclua a configuração da sua assinatura do OpenObserve", + "invalidLink": "Link de registro inválido", + "invalidLinkDescription": "Este link de registro é inválido ou expirou. Inscreva-se novamente no AWS Marketplace.", + "goToMarketplace": "Acesse o AWS Marketplace", + "newUser": "Novo no OpenObserve?", + "newUserDescription": "Crie uma nova conta e organização", + "createAccount": "Criar conta", + "existingUser": "Já tem uma conta?", + "existingUserDescription": "Faça login para criar uma nova organização com o faturamento da AWS", + "login": "Login", + "loggedInAs": "Logado como", + "createNewOrg": "Criar nova organização", + "createNewOrgDescription": "Crie uma nova organização com o faturamento do AWS Marketplace", + "orgName": "Nome da organização", + "createOrganization": "Criar organização", + "linkToExisting": "Link para a organização existente", + "linkToExistingDescription": "Vincule o faturamento da AWS a uma de suas organizações de nível gratuito", + "selectOrganization": "Selecione a organização", + "linkAwsBilling": "Vincular o faturamento da AWS", + "settingUp": "Configurando sua assinatura...", + "pleaseWait": "Aguarde enquanto configuramos sua conta.", + "waitingConfirmation": "Aguardando a confirmação da AWS", + "waitingDescription": "Sua assinatura está sendo ativada. Isso geralmente leva menos de um minuto.", + "customerId": "ID do cliente", + "subscriptionActivated": "Assinatura ativada!", + "activatedDescription": "Sua assinatura do AWS Marketplace agora está ativa.", + "goToDashboard": "Vá para o painel", + "paymentFailed": "Falha no pagamento", + "paymentFailedDescription": "Houve um problema com seu pagamento do AWS Marketplace. Verifique sua conta da AWS ou entre em contato com o suporte da AWS.", + "contactSupport": "Contate o suporte", + "tryAgain": "Tente novamente", + "activationTimeout": "Tempo limite de ativação. Entre em contato com o suporte se o problema persistir." } } \ No newline at end of file diff --git a/web/src/locales/languages/tr.json b/web/src/locales/languages/tr.json index 2c2de6e4e3..855d94336f 100644 --- a/web/src/locales/languages/tr.json +++ b/web/src/locales/languages/tr.json @@ -2684,5 +2684,40 @@ "clearChatTitle": "Sohbeti Temizle", "clearChatMessage": "Bu konuşmayı temizlemek istediğinden emin misin?", "copiedToClipboard": "Panoya kopyalandı" + }, + "awsMarketplace": { + "title": "AWS Marketplace Kaydı", + "subtitle": "OpenObserve abonelik kurulumunuzu tamamlayın", + "invalidLink": "Geçersiz Kayıt Bağlantısı", + "invalidLinkDescription": "Bu kayıt bağlantısı geçersiz veya süresi doldu. Lütfen AWS Marketplace'ten tekrar abone olun.", + "goToMarketplace": "AWS Marketplace'e gidin", + "newUser": "OpenObservere'de yeni misiniz?", + "newUserDescription": "Yeni bir hesap ve kuruluş oluşturun", + "createAccount": "Hesap Oluştur", + "existingUser": "Zaten bir hesabınız var mı?", + "existingUserDescription": "AWS faturalandırması ile yeni bir kuruluş oluşturmak için oturum açın", + "login": "Oturum aç", + "loggedInAs": "Olarak oturum açtı", + "createNewOrg": "Yeni Organizasyon Oluştur", + "createNewOrgDescription": "AWS Marketplace faturalandırması ile yeni bir kuruluş oluşturun", + "orgName": "Kuruluş Adı", + "createOrganization": "Organizasyon Oluştur", + "linkToExisting": "Mevcut Kuruluşa Bağlantı", + "linkToExistingDescription": "AWS faturalandırmasını ücretsiz katmanlı kuruluşlarınızdan birine bağlayın", + "selectOrganization": "Organizasyon Seçin", + "linkAwsBilling": "AWS Faturalandırmayı Bağlayın", + "settingUp": "Aboneliğinizi ayarlama...", + "pleaseWait": "Hesabınızı yapılandırırken lütfen bekleyin.", + "waitingConfirmation": "AWS Onayı Bekliyor", + "waitingDescription": "Aboneliğiniz etkinleştiriliyor. Bu genellikle bir dakikadan az sürer.", + "customerId": "Müşteri Kimliği", + "subscriptionActivated": "Abonelik Etkinleştirildi!", + "activatedDescription": "AWS Marketplace aboneliğiniz artık aktif.", + "goToDashboard": "Kontrol Paneline Git", + "paymentFailed": "Ödeme Başarısız", + "paymentFailedDescription": "AWS Marketplace ödemenizle ilgili bir sorun oluştu. Lütfen AWS hesabınızı kontrol edin veya AWS desteğiyle iletişime geçin.", + "contactSupport": "Destek ile iletişime geçin", + "tryAgain": "Tekrar dene", + "activationTimeout": "Aktivasyon zaman aşımı. Sorun devam ederse lütfen destek ekibiyle iletişime geçin." } } \ No newline at end of file diff --git a/web/src/locales/languages/zh.json b/web/src/locales/languages/zh.json index efeef12520..8be9a23a83 100644 --- a/web/src/locales/languages/zh.json +++ b/web/src/locales/languages/zh.json @@ -2680,5 +2680,40 @@ "clearChatTitle": "清除聊天", "clearChatMessage": "你确定要清除这个对话吗?", "copiedToClipboard": "已复制到剪贴板" + }, + "awsMarketplace": { + "title": "AWS 市场注册", + "subtitle": "完成 OpenObserve 订阅设置", + "invalidLink": "注册链接无效", + "invalidLinkDescription": "此注册链接无效或已过期。请再次从 AWS Marketplace 订阅。", + "goToMarketplace": "前往 AWS 市场", + "newUser": "OpenObserve 的新手", + "newUserDescription": "创建新账户和组织", + "createAccount": "创建账户", + "existingUser": "已经有账号了吗?", + "existingUserDescription": "登录使用 AWS 账单创建新组织", + "login": "登录", + "loggedInAs": "以身份登录", + "createNewOrg": "创建新组织", + "createNewOrgDescription": "使用 AWS Marketplace 账单创建新组织", + "orgName": "组织名称", + "createOrganization": "创建组织", + "linkToExisting": "链接到现有组织", + "linkToExistingDescription": "将 AWS 账单关联到您的一个免费套餐组织", + "selectOrganization": "选择组织", + "linkAwsBilling": "关联 AWS 账单", + "settingUp": "正在设置您的订阅...", + "pleaseWait": "我们正在配置您的账户,请稍候。", + "waitingConfirmation": "等待 AWS 确认", + "waitingDescription": "您的订阅已激活。这通常需要不到一分钟。", + "customerId": "客户 ID", + "subscriptionActivated": "订阅已激活!", + "activatedDescription": "您的 AWS 商城订阅现已激活。", + "goToDashboard": "前往控制面板", + "paymentFailed": "付款失败", + "paymentFailedDescription": "您的 AWS 商城付款出现问题。请检查您的 AWS 账户或联系 AWS 支持。", + "contactSupport": "联系支持部门", + "tryAgain": "再试一次", + "activationTimeout": "激活超时。如果问题仍然存在,请联系支持部门。" } } \ No newline at end of file diff --git a/web/src/services/awsMarketplace.ts b/web/src/services/awsMarketplace.ts new file mode 100644 index 0000000000..d3f174ab58 --- /dev/null +++ b/web/src/services/awsMarketplace.ts @@ -0,0 +1,64 @@ +// Copyright 2025 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import http from "./http"; + +export interface LinkSubscriptionRequest { + token: string; +} + +export interface LinkSubscriptionResponse { + success: boolean; + customer_identifier: string; + message?: string; +} + +export interface ActivationStatusResponse { + status: "pending_activation" | "active" | "payment_failed" | "cancelled"; + customer_identifier: string; + org_id: string; + message?: string; + activated_at?: string; +} + +const awsMarketplace = { + /** + * Link an AWS Marketplace subscription to an organization + * Called after user is redirected from AWS Marketplace with a token + */ + linkSubscription: ( + org_identifier: string, + token: string + ): Promise<{ data: LinkSubscriptionResponse }> => { + return http().post(`/api/${org_identifier}/aws-marketplace/link-subscription`, { + token, + }); + }, + + /** + * Check the activation status of an AWS Marketplace subscription + * Poll this endpoint to wait for subscribe-success SNS notification + */ + getActivationStatus: ( + org_identifier: string, + customer_id: string + ): Promise<{ data: ActivationStatusResponse }> => { + return http().get( + `/api/${org_identifier}/aws-marketplace/activation-status?customer_id=${customer_id}` + ); + }, +}; + +export default awsMarketplace; diff --git a/web/src/services/azureMarketplace.ts b/web/src/services/azureMarketplace.ts new file mode 100644 index 0000000000..393d6486e4 --- /dev/null +++ b/web/src/services/azureMarketplace.ts @@ -0,0 +1,51 @@ +// Copyright 2025 OpenObserve Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import http from "./http"; + +export interface LinkSubscriptionRequest { + token: string; +} + +export interface LinkSubscriptionResponse { + success: boolean; + customer_identifier: string; + message?: string; +} + +export interface ActivationStatusResponse { + status: "active" | "payment_failed" | "cancelled"; + customer_identifier: string; + org_id: string; + message?: string; + activated_at?: string; +} + +const azureMarketplace = { + /** + * Link an Azure Marketplace subscription to an organization + * Called after user is redirected from Azure Marketplace with a token + */ + linkSubscription: ( + org_identifier: string, + token: string + ): Promise<{ data: LinkSubscriptionResponse }> => { + return http().post(`/api/${org_identifier}/azure-marketplace/link-subscription`, { + token, + }); + } +}; + +export default azureMarketplace; \ No newline at end of file diff --git a/web/src/views/AwsMarketplaceSetup.vue b/web/src/views/AwsMarketplaceSetup.vue new file mode 100644 index 0000000000..b3cc91c35d --- /dev/null +++ b/web/src/views/AwsMarketplaceSetup.vue @@ -0,0 +1,445 @@ + + + + + + + diff --git a/web/src/views/AzureMarketplaceSetup.vue b/web/src/views/AzureMarketplaceSetup.vue new file mode 100644 index 0000000000..394349522a --- /dev/null +++ b/web/src/views/AzureMarketplaceSetup.vue @@ -0,0 +1,371 @@ + + + + + + + \ No newline at end of file diff --git a/web/src/views/Login.spec.ts b/web/src/views/Login.spec.ts index 879eeafa3e..ae2258c844 100644 --- a/web/src/views/Login.spec.ts +++ b/web/src/views/Login.spec.ts @@ -455,21 +455,33 @@ describe("Login.vue", () => { }); it("should redirect to sessionStorage URI when available", async () => { - mockSessionStorage.getItem.mockReturnValue("/dashboard"); + // Mock azure_marketplace_token as null so it checks redirectURI + mockSessionStorage.getItem.mockImplementation((key: string) => { + if (key === "azure_marketplace_token") return null; + if (key === "redirectURI") return "/dashboard"; + return null; + }); const pushSpy = vi.spyOn(router, "push"); wrapper.vm.redirectUser(); + expect(mockSessionStorage.getItem).toHaveBeenCalledWith("azure_marketplace_token"); expect(mockSessionStorage.getItem).toHaveBeenCalledWith("redirectURI"); expect(mockSessionStorage.removeItem).toHaveBeenCalledWith("redirectURI"); expect(pushSpy).toHaveBeenCalledWith({ path: "/dashboard" }); }); it("should redirect to external URL when redirectURI contains http", async () => { - mockSessionStorage.getItem.mockReturnValue("https://external.com"); + // Mock azure_marketplace_token as null so it checks redirectURI + mockSessionStorage.getItem.mockImplementation((key: string) => { + if (key === "azure_marketplace_token") return null; + if (key === "redirectURI") return "https://external.com"; + return null; + }); wrapper.vm.redirectUser(); + expect(mockSessionStorage.getItem).toHaveBeenCalledWith("azure_marketplace_token"); expect(mockSessionStorage.getItem).toHaveBeenCalledWith("redirectURI"); expect(mockSessionStorage.removeItem).toHaveBeenCalledWith("redirectURI"); expect(window.location.href).toBe("https://external.com"); @@ -494,7 +506,12 @@ describe("Login.vue", () => { }); it("should always remove redirectURI from sessionStorage", async () => { - mockSessionStorage.getItem.mockReturnValue("/some-path"); + // Mock azure_marketplace_token as null so it checks redirectURI + mockSessionStorage.getItem.mockImplementation((key: string) => { + if (key === "azure_marketplace_token") return null; + if (key === "redirectURI") return "/some-path"; + return null; + }); wrapper.vm.redirectUser(); diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index 9527cadffd..630fc36633 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -209,11 +209,32 @@ export default defineComponent({ }, 1000); }; + /** + * Helper to get cookie value by name + */ + const getCookie = (name: string): string | null => { + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + return match ? decodeURIComponent(match[2]) : null; + }; + /** * Redirect user to the page where user was redirected from * @param redirectURI */ const redirectUser = () => { + // Check for AWS Marketplace token (set by backend as cookie) - redirect to setup if present + const awsMarketplaceToken = getCookie("aws_marketplace_token"); + if (awsMarketplaceToken) { + router.push({ path: "/marketplace/aws/setup" }); + return; + } + // Check for Azure Marketplace token - redirect to setup if present + const azureMarketplaceToken = window.sessionStorage.getItem("azure_marketplace_token"); + if (azureMarketplaceToken) { + router.push({ path: "/marketplace/azure/register" }); + return; + } + const redirectURI = window.sessionStorage.getItem("redirectURI"); window.sessionStorage.removeItem("redirectURI"); if (redirectURI != null && redirectURI != "") {