mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
fix: sync saas changes (#10188)
### **User description** This sync the aws and azure saas related changes and cloud org related changes from v0.50.0 branch to main. Reviewers, please specifically take a look at [fix: auxm related changes](https://github.com/openobserve/openobserve/commit/1e278e9b5121d2df9757b3908ab27801bdc06383) commit, which does changes needed for auxm from actix web. Note that router changes have been done in the respective saas commit itself, as it caused conflicts. NOTE: This has not built locally to validate that everything at least compiles, as at the time of writing the main branch is not compiling with ent. ___ ### **PR Type** Enhancement ___ ### **Description** - AWS Marketplace onboarding: UI, API routes, services - Azure Marketplace onboarding: UI, API routes, services - Billing UI adapts to AWS/Azure providers - Add HttpResponse.created/found helpers - Add distributed metering lock and cloud jobs - Enforce free-tier org creation limits ___ <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Configuration changes</strong></td><td><details><summary>3 files</summary><table> <tr> <td><strong>router.ts</strong><dd><code>Add AWS/Azure marketplace setup routes</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-5cf8b2f94d45462f216d1b74cec6e139cbc94e6f5677bba1e7b55a30aaa95792">+31/-1</a> </td> </tr> <tr> <td><strong>mod.rs</strong><dd><code>Expose aws and azure marketplace modules</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-01aa8706a5e31c9d66036669676b55c4f50e8acbbd3323a1979dc43b9611b9a2">+2/-0</a> </td> </tr> <tr> <td><strong>mod.rs</strong><dd><code>Register marketplace webhook and API routes</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-02556e3f515441b5d2b6bbea7829df488beac03c4cc41309a6638ae3c0e0aec6">+23/-1</a> </td> </tr> </table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>16 files</summary><table> <tr> <td><strong>awsMarketplace.ts</strong><dd><code>Implement AWS Marketplace HTTP client</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-a84cf89c9eec197f49a3eafff23d45d539f711282b60175a80cb96255e877661">+64/-0</a> </td> </tr> <tr> <td><strong>azureMarketplace.ts</strong><dd><code>Implement Azure Marketplace HTTP client</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-45edda85f9d7b0e2ec5a750d112b0b52d22dce0c48aa8361e30d51cacfeee16c">+51/-0</a> </td> </tr> <tr> <td><strong>http.rs</strong><dd><code>Add HttpResponse.created and found helpers</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-e1831572a854319ed0a69e803514466aea13a4924f4fa02f58dbf508c30a19fd">+18/-0</a> </td> </tr> <tr> <td><strong>billings.rs</strong><dd><code>Include provider field in subscription model</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-986786190d086283f70a66cc487eadc68784dd4200f8a32e6cf4c50a3d6603ca">+3/-0</a> </td> </tr> <tr> <td><strong>aws_marketplace.rs</strong><dd><code>Add AWS Marketplace registration and linking handlers</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-03b179d714809fbccd4a85bbd783f2399540b515796f5afb0060ff886f872b87">+364/-0</a> </td> </tr> <tr> <td><strong>azure_marketplace.rs</strong><dd><code>Add Azure Marketplace linking handler</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-da9cf01b26c5f642ceebba9d4355bc45ccb518b4a191c38dbe3178f42884b153">+148/-0</a> </td> </tr> <tr> <td><strong>billings.rs</strong><dd><code>Enhance billing webhook and redirect logging</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-725936e3d6e955175e6100e8ca6f4437f334a2b554d6f11fa349ee9cbac70425">+64/-5</a> </td> </tr> <tr> <td><strong>mod.rs</strong><dd><code>Add distributed metering lock logic</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-f2e4c29b8eef22189ec704a832633c4bd135a24444f054864c98064bd0963681">+67/-2</a> </td> </tr> <tr> <td><strong>organization.rs</strong><dd><code>Enforce free org limits in org workflow</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-6b3c3c2b027c1158c0d41302cc69dfcd20ab68a6a4fa365cda403f838940bbb4">+40/-13</a> </td> </tr> <tr> <td><strong>Billing.vue</strong><dd><code>Conditional invoice tab based on provider</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-08d804667873a5d984027ad69438432a14973e0318cf78ad981bc7b9a31c1bf4">+30/-2</a> </td> </tr> <tr> <td><strong>TrialPeriod.vue</strong><dd><code>Hide trial message for AWS-billed orgs</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-cc999442352001a0c7a756db0974f99ec3a21d566e9082d1b113c85ca897dafa">+25/-3</a> </td> </tr> <tr> <td><strong>plans.vue</strong><dd><code>Adapt plan view to billing providers</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-a85ef89681b89f6ae045ed1c45bd3e4aa8c494a7784a8a56514def1af6eef896">+6/-1</a> </td> </tr> <tr> <td><strong>proPlan.vue</strong><dd><code>Display AWS/Azure billing chip in plan card</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-b5678b2763909484d6475adf5af690b3d382d8438520adcfc1428b66cf7ad7d5">+28/-2</a> </td> </tr> <tr> <td><strong>AwsMarketplaceSetup.vue</strong><dd><code>Add AWS Marketplace setup Vue view</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-7345a61d1ba6216718c48067d9c010f069fb43222a15094b4644474eb9091cae">+445/-0</a> </td> </tr> <tr> <td><strong>AzureMarketplaceSetup.vue</strong><dd><code>Add Azure Marketplace setup Vue view</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-5e29364b67f1e0a714d1d219bc4b619229068ef6e0b2312cedeabf720a0b25f1">+371/-0</a> </td> </tr> <tr> <td><strong>Login.vue</strong><dd><code>Support marketplace redirects in login flow</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-a135c379ef6ac8f92199ea5f7ff19a227971a9b0a87ccccf87c0268740522c2c">+21/-0</a> </td> </tr> </table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>1 files</summary><table> <tr> <td><strong>en.json</strong><dd><code>Add AWS/Azure marketplace localization text</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10188/files#diff-892aa3d75ab6e129ef9116aa21bec1b9a15ba69ce5366d2a19128078c690d824">+35/-0</a> </td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___ --------- Signed-off-by: Yashodhan Joshi <yjdoc2@gmail.com> Co-authored-by: Yashodhan Joshi <yjdoc2@gmail.com> Co-authored-by: udev <uddhav@openobserve.ai> Co-authored-by: sai nikhil kethe <nikhil@openobserve.ai> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Yashodhan Joshi
udev
sai nikhil kethe
github-actions[bot]
parent
5426673cbd
commit
6b29f3cdc9
@@ -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<String>) -> 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 {
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct ListSubscriptionResponseBody {
|
||||
pub subscription_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub customer_id: Option<String>,
|
||||
/// Billing provider: "stripe" or "aws"
|
||||
pub provider: String,
|
||||
}
|
||||
|
||||
impl From<cloud_billings::CustomerBilling> for ListSubscriptionResponseBody {
|
||||
@@ -46,6 +48,7 @@ impl From<cloud_billings::CustomerBilling> for ListSubscriptionResponseBody {
|
||||
Self {
|
||||
subscription_type: value.subscription_type.to_string(),
|
||||
customer_id: value.customer_id,
|
||||
provider: value.provider.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<String>,
|
||||
#[serde(rename = "x-amzn-marketplace-product-id")]
|
||||
pub product_id: Option<String>,
|
||||
#[serde(rename = "x-amzn-marketplace-fulfillment-url")]
|
||||
pub fulfillment_url: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<AwsMarketplaceRegistrationForm>,
|
||||
) -> 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<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
Headers(user_email): Headers<UserEmail>,
|
||||
axum::Json(req): axum::Json<LinkSubscriptionRequest>,
|
||||
) -> 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<String>, 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<String>,
|
||||
Query(query): Query<std::collections::HashMap<String, String>>,
|
||||
Headers(user_email): Headers<UserEmail>,
|
||||
) -> 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)
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<String>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
Headers(user_email): Headers<UserEmail>,
|
||||
axum::Json(req): axum::Json<LinkSubscriptionRequest>,
|
||||
) -> 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")
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+67
-2
@@ -80,6 +80,62 @@ async fn enforce_usage_stream_retention() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
async fn get_metering_lock() -> Result<Option<()>, 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() {
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
}
|
||||
|
||||
+40
-13
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -62,45 +62,46 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
vertical
|
||||
>
|
||||
|
||||
<q-route-tab
|
||||
exact
|
||||
name="plans"
|
||||
:to="
|
||||
'/billings/plans?org_identifier=' +
|
||||
store.state.selectedOrganization.identifier
|
||||
"
|
||||
:icon="'img:' + getImageURL('images/common/plan_icon.svg')"
|
||||
:label="t('billing.plansLabel')"
|
||||
content-class="tab_content"
|
||||
/>
|
||||
<q-route-tab
|
||||
exact
|
||||
default
|
||||
name="usage"
|
||||
:to="
|
||||
'/billings/usage?org_identifier=' +
|
||||
store.state.selectedOrganization.identifier +
|
||||
'&usage_date=' +
|
||||
usageDate +
|
||||
'&data_type=' +
|
||||
usageDataType
|
||||
"
|
||||
:icon="'img:' + getImageURL('images/common/usage_icon.svg')"
|
||||
:label="t('billing.usageLabel')"
|
||||
content-class="tab_content"
|
||||
/>
|
||||
<q-route-tab
|
||||
exact
|
||||
name="invoice_history"
|
||||
:to="
|
||||
'/billings/invoice_history?org_identifier=' +
|
||||
store.state.selectedOrganization.identifier
|
||||
"
|
||||
:icon="'img:' + getImageURL('images/common/invoice_icon.svg')"
|
||||
:label="t('billing.invoiceHistoryLabel')"
|
||||
content-class="tab_content"
|
||||
/>
|
||||
</q-tabs>
|
||||
<q-route-tab
|
||||
exact
|
||||
name="plans"
|
||||
:to="
|
||||
'/billings/plans?org_identifier=' +
|
||||
store.state.selectedOrganization.identifier
|
||||
"
|
||||
:icon="'img:' + getImageURL('images/common/plan_icon.svg')"
|
||||
:label="t('billing.plansLabel')"
|
||||
content-class="tab_content"
|
||||
/>
|
||||
<q-route-tab
|
||||
exact
|
||||
default
|
||||
name="usage"
|
||||
:to="
|
||||
'/billings/usage?org_identifier=' +
|
||||
store.state.selectedOrganization.identifier +
|
||||
'&usage_date=' +
|
||||
usageDate +
|
||||
'&data_type=' +
|
||||
usageDataType
|
||||
"
|
||||
:icon="'img:' + getImageURL('images/common/usage_icon.svg')"
|
||||
:label="t('billing.usageLabel')"
|
||||
content-class="tab_content"
|
||||
/>
|
||||
<q-route-tab
|
||||
v-if="showInvoiceTab"
|
||||
exact
|
||||
name="invoice_history"
|
||||
:to="
|
||||
'/billings/invoice_history?org_identifier=' +
|
||||
store.state.selectedOrganization.identifier
|
||||
"
|
||||
:icon="'img:' + getImageURL('images/common/invoice_icon.svg')"
|
||||
:label="t('billing.invoiceHistoryLabel')"
|
||||
content-class="tab_content"
|
||||
/>
|
||||
</q-tabs>
|
||||
<!-- <q-btn
|
||||
data-test="logs-search-field-list-collapse-btn"
|
||||
:icon="showSidebar ? 'chevron_left' : 'chevron_right'"
|
||||
@@ -140,6 +141,7 @@ import ConfirmDialog from "@/components/ConfirmDialog.vue";
|
||||
import Usage from "./usage.vue";
|
||||
import { getImageURL } from "@/utils/zincutils";
|
||||
import AppTabs from "@/components/common/AppTabs.vue";
|
||||
import BillingService from "@/services/billings";
|
||||
|
||||
export default defineComponent({
|
||||
name: "PageIngestion",
|
||||
@@ -154,6 +156,28 @@ export default defineComponent({
|
||||
const showSidebar = ref(true);
|
||||
const lastSplitterPosition = ref(200);
|
||||
const splitterModel = ref(220);
|
||||
const billingProvider = ref(""); // empty until loaded
|
||||
const billingInfoLoaded = ref(false);
|
||||
|
||||
// Fetch billing info to determine provider
|
||||
const fetchBillingInfo = async () => {
|
||||
try {
|
||||
const res = await BillingService.list_subscription(
|
||||
store.state.selectedOrganization.identifier
|
||||
);
|
||||
billingProvider.value = res.data?.provider || "";
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch billing info:", e);
|
||||
billingProvider.value = "";
|
||||
} finally {
|
||||
billingInfoLoaded.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Check if invoice tab should be shown (only for Stripe, and only after loading)
|
||||
const showInvoiceTab = computed(() => {
|
||||
return billingInfoLoaded.value && billingProvider.value === "stripe";
|
||||
});
|
||||
const collapseSidebar = () => {
|
||||
showSidebar.value = !showSidebar.value;
|
||||
if (showSidebar.value) {
|
||||
@@ -164,7 +188,9 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// Fetch billing info to determine provider type
|
||||
await fetchBillingInfo();
|
||||
if (router.currentRoute.value.name == "billings" || router.currentRoute.value.name == "plans") {
|
||||
billingtab.value = "plans";
|
||||
router.push({ path: "/billings/plans", query: { org_identifier: store.state.selectedOrganization.identifier } });
|
||||
@@ -226,7 +252,7 @@ export default defineComponent({
|
||||
splitterModel,
|
||||
headerBasedOnRoute,
|
||||
options: [
|
||||
{label: "30 Days", value: "30days"},
|
||||
{label: "30 Days", value: "30days"},
|
||||
{label: "60 Days", value: "60days"},
|
||||
{label: "3 Months", value: "3months"},
|
||||
{label: "6 Months", value: "6months"}],
|
||||
@@ -239,6 +265,8 @@ export default defineComponent({
|
||||
collapseSidebar,
|
||||
showSidebar,
|
||||
lastSplitterPosition,
|
||||
showInvoiceTab,
|
||||
billingProvider,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-ignore
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { defineComponent, ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useStore } from "vuex";
|
||||
import { useRouter } from "vue-router";
|
||||
@@ -50,6 +50,8 @@ import { useQuasar } from "quasar";
|
||||
import config from "@/aws-exports";
|
||||
import { siteURL } from "@/constants/config";
|
||||
import { getDueDays } from "@/utils/zincutils";
|
||||
import BillingService from "@/services/billings";
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
name: "TrialPeriod",
|
||||
@@ -76,8 +78,28 @@ export default defineComponent({
|
||||
const q = useQuasar();
|
||||
const router: any = useRouter();
|
||||
|
||||
const showTrialPeriodMsg = ref((Object.hasOwn(store.state.organizationData.organizationSettings, "free_trial_expiry") && store.state.organizationData.organizationSettings.free_trial_expiry != "" && store.state.organizationData.organizationSettings.free_trial_expiry != null) ? true : false);
|
||||
|
||||
const hasTrialExpiry = Object.hasOwn(store.state.organizationData.organizationSettings, "free_trial_expiry")
|
||||
&& store.state.organizationData.organizationSettings.free_trial_expiry != ""
|
||||
&& store.state.organizationData.organizationSettings.free_trial_expiry != null;
|
||||
|
||||
const showTrialPeriodMsg = ref(hasTrialExpiry);
|
||||
|
||||
// Check if org is on AWS billing - don't show trial message for AWS orgs
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await BillingService.list_subscription(
|
||||
store.state.selectedOrganization.identifier
|
||||
);
|
||||
if (res.data?.provider === "aws") {
|
||||
// AWS billing - don't show trial period message
|
||||
showTrialPeriodMsg.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
// If fetch fails, keep the default behavior
|
||||
console.error("Failed to fetch billing info:", e);
|
||||
}
|
||||
});
|
||||
|
||||
const redirectBilling = () => {
|
||||
router.push('/billings/plans/')
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<div v-else class="row q-gutter-md justify-center">
|
||||
<pro-plan
|
||||
:planType="planType"
|
||||
:billingProvider="billingProvider"
|
||||
@update:proSubscription="onLoadSubscription(config.paidPlan)"
|
||||
@update:cancelSubscription="onUnsubscribe"
|
||||
></pro-plan>
|
||||
@@ -137,6 +138,7 @@ export default defineComponent({
|
||||
try{
|
||||
const res = await BillingService.list_subscription(this.store.state.selectedOrganization.identifier);
|
||||
this.currentPlanDetail = res.data;
|
||||
this.billingProvider = res.data.provider || "";
|
||||
|
||||
if (res.data.subscription_type !== "") {
|
||||
if (res.data.subscription_type == config.paidPlan) {
|
||||
@@ -152,7 +154,8 @@ export default defineComponent({
|
||||
useLocalOrganization(localOrg.value);
|
||||
this.store.dispatch("setSelectedOrganization", localOrg.value);
|
||||
}
|
||||
} else {
|
||||
} else if (this.billingProvider === "" || this.billingProvider === "stripe") {
|
||||
// Only show subscribe prompt for Stripe orgs without subscription
|
||||
this.$q.notify({
|
||||
type: "warning",
|
||||
message: "Please subscribe to one of the plan.",
|
||||
@@ -193,6 +196,7 @@ export default defineComponent({
|
||||
const listSubscriptionResponse: any = ref({});
|
||||
const proLoading: any = ref(false);
|
||||
const currentPlanDetail = ref();
|
||||
const billingProvider = ref("");
|
||||
|
||||
const retrieveHostedPage = () => {
|
||||
BillingService.retrieve_hosted_page(
|
||||
@@ -221,6 +225,7 @@ export default defineComponent({
|
||||
retrieveHostedPage,
|
||||
proLoading,
|
||||
currentPlanDetail,
|
||||
billingProvider,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,8 +61,34 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
|
||||
<div class="row justify-between q-pa-md">
|
||||
<!-- AWS Marketplace billing - show managed externally message -->
|
||||
<div v-if="billingProvider === 'aws'" class="full-width text-center">
|
||||
<q-chip
|
||||
color="green-2"
|
||||
text-color="green-10"
|
||||
icon="check_circle"
|
||||
label="Managed via AWS Marketplace"
|
||||
class="q-px-md q-py-sm"
|
||||
/>
|
||||
<div class="text-caption text-grey-7 q-mt-sm">
|
||||
Billing is handled through your AWS account
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="billingProvider === 'azure'" class="full-width text-center">
|
||||
<q-chip
|
||||
color="green-2"
|
||||
text-color="green-10"
|
||||
icon="check_circle"
|
||||
label="Managed via Azure Marketplace"
|
||||
class="q-px-md q-py-sm"
|
||||
/>
|
||||
<div class="text-caption text-grey-7 q-mt-sm">
|
||||
Billing is handled through your Azure account
|
||||
</div>
|
||||
</div>
|
||||
<!-- Stripe billing - show subscribe/manage buttons -->
|
||||
<q-btn
|
||||
v-if="planType == planName"
|
||||
v-else-if="planType == planName"
|
||||
:label="btnCancelSubscription"
|
||||
text-color="black"
|
||||
class="full-width bg-grey-4 text-bold text-capitalize text-subtitle1"
|
||||
@@ -87,7 +113,7 @@ import { useI18n } from "vue-i18n";
|
||||
|
||||
export default defineComponent({
|
||||
name: "proPlan",
|
||||
props: ["planType"],
|
||||
props: ["planType", "billingProvider"],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const planName = "pay-as-you-go";
|
||||
|
||||
@@ -17,8 +17,38 @@ import Billing from "@/enterprise/components/billings/Billing.vue";
|
||||
import Plans from "@/enterprise/components/billings/plans.vue";
|
||||
import InvoiceHistory from "@/enterprise/components/billings/invoiceHistory.vue";
|
||||
import Usage from "@/enterprise/components/billings/usage.vue";
|
||||
import AzureMarketplaceSetup from "@/views/AzureMarketplaceSetup.vue";
|
||||
import AwsMarketplaceSetup from "@/views/AwsMarketplaceSetup.vue";
|
||||
|
||||
const useEnvRoutes = () => {
|
||||
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 = [
|
||||
{
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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": "アクティベーションタイムアウト。問題が解決しない場合は、サポートに連絡してください。"
|
||||
}
|
||||
}
|
||||
@@ -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": "활성화 제한 시간이 초과되었습니다.문제가 지속되면 지원팀에 문의하세요."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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": "激活超时。如果问题仍然存在,请联系支持部门。"
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,445 @@
|
||||
<!-- 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 <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="aws-marketplace-setup">
|
||||
<div class="flex relative-position tw-px-3 tw-pt-2">
|
||||
<img
|
||||
class="appLogo"
|
||||
loading="lazy"
|
||||
:src="
|
||||
store?.state?.theme === 'dark'
|
||||
? getImageURL('images/common/openobserve_latest_dark_2.svg')
|
||||
: getImageURL('images/common/openobserve_latest_light_2.svg')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setup-container q-pa-xl">
|
||||
<!-- No Token Error -->
|
||||
<div v-if="state === 'no_token'" class="text-center">
|
||||
<q-icon name="warning" size="80px" color="warning" />
|
||||
<h5 class="q-mt-md">No Marketplace Token Found</h5>
|
||||
<p class="text-grey-7">
|
||||
Please start the registration process from AWS Marketplace.
|
||||
</p>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Go to Dashboard"
|
||||
@click="goToDashboard"
|
||||
class="q-mt-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="state === 'error'" class="text-center">
|
||||
<q-icon name="error" size="80px" color="negative" />
|
||||
<h5 class="q-mt-md">{{ errorMessage }}</h5>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Try Again"
|
||||
@click="resetAndRetry"
|
||||
class="q-mt-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Org Selection/Creation -->
|
||||
<div v-else-if="state === 'select_org'" class="text-center">
|
||||
<q-icon name="cloud" size="60px" color="primary" />
|
||||
<h4 class="q-mt-md">Complete AWS Marketplace Setup</h4>
|
||||
<p class="text-grey-7 q-mb-lg">
|
||||
Link your AWS Marketplace subscription to an organization
|
||||
</p>
|
||||
|
||||
<div class="options-container">
|
||||
<!-- Create New Org -->
|
||||
<q-card flat bordered class="option-card q-mb-md">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Create New Organization</div>
|
||||
<p class="text-grey-7">
|
||||
Create a new organization with AWS Marketplace billing
|
||||
</p>
|
||||
<q-input
|
||||
v-model="newOrgName"
|
||||
label="Organization Name"
|
||||
outlined
|
||||
dense
|
||||
class="q-mb-md"
|
||||
:rules="[(val) => !!val || 'Organization name is required']"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Create & Link"
|
||||
@click="createNewOrgWithAws"
|
||||
:loading="isProcessing"
|
||||
:disable="!newOrgName"
|
||||
class="full-width"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- Link to Existing Org (only show orgs without billing) -->
|
||||
<q-card
|
||||
v-if="eligibleOrganizations.length > 0"
|
||||
flat
|
||||
bordered
|
||||
class="option-card"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Link to Existing Organization</div>
|
||||
<p class="text-grey-7">
|
||||
Link AWS billing to an existing organization
|
||||
</p>
|
||||
<q-select
|
||||
v-model="selectedOrg"
|
||||
:options="eligibleOrganizations"
|
||||
option-label="name"
|
||||
option-value="identifier"
|
||||
label="Select Organization"
|
||||
outlined
|
||||
dense
|
||||
class="q-mb-md"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Link AWS Billing"
|
||||
@click="linkToExistingOrg"
|
||||
:loading="isProcessing"
|
||||
:disable="!selectedOrg"
|
||||
class="full-width"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing State -->
|
||||
<div v-else-if="state === 'processing'" class="text-center">
|
||||
<q-spinner-dots size="60px" color="primary" />
|
||||
<h5 class="q-mt-md">Setting up your subscription...</h5>
|
||||
<p class="text-grey-7">Please wait while we configure your account.</p>
|
||||
</div>
|
||||
|
||||
<!-- Pending Activation State -->
|
||||
<div v-else-if="state === 'pending_activation'" class="text-center">
|
||||
<h5 class="q-mb-lg">Waiting for AWS Confirmation</h5>
|
||||
<div class="flex justify-center">
|
||||
<q-spinner-gears size="80px" color="primary" />
|
||||
</div>
|
||||
<p class="text-grey-7 q-mt-lg">
|
||||
Please wait while we confirm activation with AWS and set up your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Success State -->
|
||||
<div v-else-if="state === 'success'" class="text-center">
|
||||
<q-icon name="check_circle" size="80px" color="positive" />
|
||||
<h4 class="q-mt-md">Subscription Activated!</h4>
|
||||
<p class="text-grey-7">
|
||||
Your AWS Marketplace subscription is now active.
|
||||
</p>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Go to Dashboard"
|
||||
@click="goToDashboard"
|
||||
class="q-mt-lg"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Payment Failed State -->
|
||||
<div v-else-if="state === 'payment_failed'" class="text-center">
|
||||
<q-icon name="error" size="80px" color="negative" />
|
||||
<h5 class="q-mt-md">Payment Failed</h5>
|
||||
<p class="text-grey-7">
|
||||
There was an issue with your AWS Marketplace payment. Please check
|
||||
your AWS account or contact AWS support.
|
||||
</p>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Contact Support"
|
||||
href="mailto:support@openobserve.ai"
|
||||
class="q-mt-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { getImageURL, useLocalOrganization } from "@/utils/zincutils";
|
||||
import awsMarketplace from "@/services/awsMarketplace";
|
||||
import organizationsService from "@/services/organizations";
|
||||
|
||||
type SetupState =
|
||||
| "select_org"
|
||||
| "no_token"
|
||||
| "processing"
|
||||
| "pending_activation"
|
||||
| "success"
|
||||
| "payment_failed"
|
||||
| "error";
|
||||
|
||||
export default defineComponent({
|
||||
name: "AwsMarketplaceSetup",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const q = useQuasar();
|
||||
|
||||
const state = ref<SetupState>("select_org");
|
||||
const errorMessage = ref("");
|
||||
const isProcessing = ref(false);
|
||||
const newOrgName = ref("");
|
||||
const selectedOrg = ref<{ identifier: string; name: string } | null>(null);
|
||||
const eligibleOrganizations = ref<{ identifier: string; name: string }[]>(
|
||||
[]
|
||||
);
|
||||
const token = ref("");
|
||||
const activatedOrgId = ref("");
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// Helper to get cookie value
|
||||
const getCookie = (name: string): string | null => {
|
||||
const match = document.cookie.match(new RegExp('(^|; )' + name + '=([^;]+)'));
|
||||
return match ? decodeURIComponent(match[2]) : null;
|
||||
};
|
||||
|
||||
// Helper to delete cookie
|
||||
const deleteCookie = (name: string) => {
|
||||
document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// Get token from cookie (set by backend at /api/aws-marketplace/register)
|
||||
token.value = getCookie("aws_marketplace_token") || "";
|
||||
|
||||
if (!token.value) {
|
||||
state.value = "no_token";
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch user's organizations
|
||||
await fetchOrganizations();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval);
|
||||
}
|
||||
});
|
||||
|
||||
const fetchOrganizations = async () => {
|
||||
try {
|
||||
const res = await organizationsService.list(0, 100000, "id", false, "");
|
||||
// Show all orgs for now - backend will validate eligibility
|
||||
eligibleOrganizations.value = res.data.data.map((org: any) => ({
|
||||
identifier: org.identifier,
|
||||
name: org.name,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch organizations:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const createNewOrgWithAws = async () => {
|
||||
if (!newOrgName.value) {
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: "Please enter an organization name",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing.value = true;
|
||||
state.value = "processing";
|
||||
|
||||
try {
|
||||
// Create the organization
|
||||
const orgRes = await organizationsService.create({
|
||||
name: newOrgName.value,
|
||||
});
|
||||
|
||||
const orgId = orgRes.data.identifier;
|
||||
|
||||
// Link AWS Marketplace subscription
|
||||
await linkSubscription(orgId);
|
||||
} catch (error: any) {
|
||||
console.error("Failed to create organization:", error);
|
||||
state.value = "error";
|
||||
errorMessage.value =
|
||||
error.response?.data?.message || "Failed to create organization";
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const linkToExistingOrg = async () => {
|
||||
if (!selectedOrg.value) {
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: "Please select an organization",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing.value = true;
|
||||
state.value = "processing";
|
||||
|
||||
await linkSubscription(selectedOrg.value.identifier);
|
||||
};
|
||||
|
||||
const linkSubscription = async (orgId: string) => {
|
||||
try {
|
||||
const response = await awsMarketplace.linkSubscription(
|
||||
orgId,
|
||||
token.value
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
// Clear the token cookie immediately after successful link
|
||||
// The token has been used (ResolveCustomer called) and can't be reused
|
||||
deleteCookie("aws_marketplace_token");
|
||||
token.value = "";
|
||||
|
||||
activatedOrgId.value = orgId;
|
||||
state.value = "pending_activation";
|
||||
|
||||
// Start polling for activation status
|
||||
startPolling(orgId, response.data.customer_identifier);
|
||||
} else {
|
||||
throw new Error(response.data.message || "Link subscription failed");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to link subscription:", error);
|
||||
state.value = "error";
|
||||
errorMessage.value =
|
||||
error.response?.data?.message || "Failed to link AWS subscription";
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startPolling = (orgId: string, customerIdentifier: string) => {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60; // 5 minutes at 5 second intervals
|
||||
|
||||
pollInterval = setInterval(async () => {
|
||||
attempts++;
|
||||
|
||||
try {
|
||||
const response = await awsMarketplace.getActivationStatus(
|
||||
orgId,
|
||||
customerIdentifier
|
||||
);
|
||||
|
||||
const status = response.data.status;
|
||||
|
||||
if (status === "active") {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
state.value = "success";
|
||||
isProcessing.value = false;
|
||||
|
||||
// Update selected org in store
|
||||
const orgData = {
|
||||
identifier: orgId,
|
||||
label: newOrgName.value || selectedOrg.value?.name || orgId,
|
||||
user_email: store.state.userInfo?.email,
|
||||
};
|
||||
useLocalOrganization(orgData);
|
||||
store.dispatch("setSelectedOrganization", orgData);
|
||||
} else if (status === "payment_failed") {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
state.value = "payment_failed";
|
||||
isProcessing.value = false;
|
||||
} else if (attempts >= maxAttempts) {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
state.value = "error";
|
||||
errorMessage.value =
|
||||
"Activation timeout. Please contact support if the issue persists.";
|
||||
isProcessing.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Poll error:", error);
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const goToDashboard = () => {
|
||||
router.push({
|
||||
path: "/",
|
||||
query: activatedOrgId.value
|
||||
? { org_identifier: activatedOrgId.value }
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const resetAndRetry = () => {
|
||||
state.value = "select_org";
|
||||
errorMessage.value = "";
|
||||
isProcessing.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
store,
|
||||
state,
|
||||
errorMessage,
|
||||
isProcessing,
|
||||
newOrgName,
|
||||
selectedOrg,
|
||||
eligibleOrganizations,
|
||||
getImageURL,
|
||||
createNewOrgWithAws,
|
||||
linkToExistingOrg,
|
||||
goToDashboard,
|
||||
resetAndRetry,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.aws-marketplace-setup {
|
||||
min-height: 100vh;
|
||||
background: var(--q-background);
|
||||
}
|
||||
|
||||
.appLogo {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.setup-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,371 @@
|
||||
<!-- 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 <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="azure-marketplace-setup">
|
||||
<div class="flex relative-position tw-px-3 tw-pt-2">
|
||||
<img
|
||||
class="appLogo"
|
||||
loading="lazy"
|
||||
:src="
|
||||
store?.state?.theme === 'dark'
|
||||
? getImageURL('images/common/openobserve_latest_dark_2.svg')
|
||||
: getImageURL('images/common/openobserve_latest_light_2.svg')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="setup-container q-pa-xl">
|
||||
<!-- No Token Error -->
|
||||
<div v-if="state === 'no_token'" class="text-center">
|
||||
<q-icon name="warning" size="80px" color="warning" />
|
||||
<h5 class="q-mt-md">No Marketplace Token Found</h5>
|
||||
<p class="text-grey-7">
|
||||
Please start the registration process from Azure Marketplace.
|
||||
</p>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Go to Dashboard"
|
||||
@click="goToDashboard"
|
||||
class="q-mt-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="state === 'error'" class="text-center">
|
||||
<q-icon name="error" size="80px" color="negative" />
|
||||
<h5 class="q-mt-md">{{ errorMessage }}</h5>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Try Again"
|
||||
@click="resetAndRetry"
|
||||
class="q-mt-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Org Selection/Creation -->
|
||||
<div v-else-if="state === 'select_org'" class="text-center">
|
||||
<q-icon name="cloud" size="60px" color="primary" />
|
||||
<h4 class="q-mt-md">Complete Azure Marketplace Setup</h4>
|
||||
<p class="text-grey-7 q-mb-lg">
|
||||
Link your Azure Marketplace subscription to an organization
|
||||
</p>
|
||||
|
||||
<div class="options-container">
|
||||
<!-- Create New Org -->
|
||||
<q-card flat bordered class="option-card q-mb-md">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Create New Organization</div>
|
||||
<p class="text-grey-7">
|
||||
Create a new organization with Azure Marketplace billing
|
||||
</p>
|
||||
<q-input
|
||||
v-model="newOrgName"
|
||||
label="Organization Name"
|
||||
outlined
|
||||
dense
|
||||
class="q-mb-md"
|
||||
:rules="[(val) => !!val || 'Organization name is required']"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Create & Link"
|
||||
@click="createNewOrgForAzure"
|
||||
:loading="isProcessing"
|
||||
:disable="!newOrgName"
|
||||
class="full-width"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- Link to Existing Org (only show orgs without billing) -->
|
||||
<q-card
|
||||
v-if="eligibleOrganizations.length > 0"
|
||||
flat
|
||||
bordered
|
||||
class="option-card"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="text-h6">Link to Existing Organization</div>
|
||||
<p class="text-grey-7">
|
||||
Link Azure billing to an existing organization
|
||||
</p>
|
||||
<q-select
|
||||
v-model="selectedOrg"
|
||||
:options="eligibleOrganizations"
|
||||
option-label="name"
|
||||
option-value="identifier"
|
||||
label="Select Organization"
|
||||
outlined
|
||||
dense
|
||||
class="q-mb-md"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Link Azure Billing"
|
||||
@click="linkToExistingOrg"
|
||||
:loading="isProcessing"
|
||||
:disable="!selectedOrg"
|
||||
class="full-width"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing State -->
|
||||
<div v-else-if="state === 'processing'" class="text-center">
|
||||
<q-spinner-dots size="60px" color="primary" />
|
||||
<h5 class="q-mt-md">Setting up your subscription...</h5>
|
||||
<p class="text-grey-7">Please wait while we configure your account.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Success State -->
|
||||
<div v-else-if="state === 'success'" class="text-center">
|
||||
<q-icon name="check_circle" size="80px" color="positive" />
|
||||
<h4 class="q-mt-md">Subscription Activated!</h4>
|
||||
<p class="text-grey-7">
|
||||
Your Azure Marketplace subscription is now active.
|
||||
</p>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Go to Dashboard"
|
||||
@click="goToDashboard"
|
||||
class="q-mt-lg"
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Payment Failed State -->
|
||||
<div v-else-if="state === 'payment_failed'" class="text-center">
|
||||
<q-icon name="error" size="80px" color="negative" />
|
||||
<h5 class="q-mt-md">Payment Failed</h5>
|
||||
<p class="text-grey-7">
|
||||
There was an issue with activating Azure subscription. Please check
|
||||
your Azure account or contact support.
|
||||
</p>
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Contact Support"
|
||||
href="mailto:support@openobserve.ai"
|
||||
class="q-mt-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { getImageURL, useLocalOrganization } from "@/utils/zincutils";
|
||||
import azureMarketplace from "@/services/azureMarketplace";
|
||||
import organizationsService from "@/services/organizations";
|
||||
|
||||
type SetupState =
|
||||
| "select_org"
|
||||
| "no_token"
|
||||
| "processing"
|
||||
| "success"
|
||||
| "payment_failed"
|
||||
| "error";
|
||||
|
||||
export default defineComponent({
|
||||
name: "AzureMarketplaceSetup",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const q = useQuasar();
|
||||
|
||||
const state = ref<SetupState>("select_org");
|
||||
const errorMessage = ref("");
|
||||
const isProcessing = ref(false);
|
||||
const newOrgName = ref("");
|
||||
const selectedOrg = ref<{ identifier: string; name: string } | null>(null);
|
||||
const eligibleOrganizations = ref<{ identifier: string; name: string }[]>(
|
||||
[]
|
||||
);
|
||||
const token = ref("");
|
||||
const activatedOrgId = ref("");
|
||||
|
||||
onMounted(async () => {
|
||||
// Get token from sessionStorage (saved by /marketplace/azure/register route)
|
||||
token.value = sessionStorage.getItem("azure_marketplace_token") || "";
|
||||
|
||||
if (!token.value) {
|
||||
state.value = "no_token";
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch user's organizations
|
||||
await fetchOrganizations();
|
||||
});
|
||||
|
||||
const fetchOrganizations = async () => {
|
||||
try {
|
||||
const res = await organizationsService.list(0, 100000, "id", false, "");
|
||||
// Show all orgs for now - backend will validate eligibility
|
||||
eligibleOrganizations.value = res.data.data.map((org: any) => ({
|
||||
identifier: org.identifier,
|
||||
name: org.name,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch organizations:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const createNewOrgForAzure = async () => {
|
||||
if (!newOrgName.value) {
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: "Please enter an organization name",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing.value = true;
|
||||
state.value = "processing";
|
||||
|
||||
try {
|
||||
// Create the organization
|
||||
const orgRes = await organizationsService.create({
|
||||
name: newOrgName.value,
|
||||
});
|
||||
|
||||
const orgId = orgRes.data.identifier;
|
||||
|
||||
// Link AWS Marketplace subscription
|
||||
await linkSubscription(orgId);
|
||||
} catch (error: any) {
|
||||
console.error("Failed to create organization:", error);
|
||||
state.value = "error";
|
||||
errorMessage.value =
|
||||
error.response?.data?.message || "Failed to create organization";
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const linkToExistingOrg = async () => {
|
||||
if (!selectedOrg.value) {
|
||||
q.notify({
|
||||
type: "negative",
|
||||
message: "Please select an organization",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isProcessing.value = true;
|
||||
state.value = "processing";
|
||||
|
||||
await linkSubscription(selectedOrg.value.identifier);
|
||||
};
|
||||
|
||||
const linkSubscription = async (orgId: string) => {
|
||||
try {
|
||||
await azureMarketplace.linkSubscription(
|
||||
orgId,
|
||||
token.value
|
||||
);
|
||||
|
||||
// Clear the token from sessionStorage
|
||||
sessionStorage.removeItem("azure_marketplace_token");
|
||||
state.value = "success";
|
||||
isProcessing.value = false;
|
||||
|
||||
// Update selected org in store
|
||||
const orgData = {
|
||||
identifier: orgId,
|
||||
label: newOrgName.value || selectedOrg.value?.name || orgId,
|
||||
user_email: store.state.userInfo?.email,
|
||||
};
|
||||
useLocalOrganization(orgData);
|
||||
store.dispatch("setSelectedOrganization", orgData);
|
||||
} catch (error: any) {
|
||||
console.error("Failed to link subscription:", error);
|
||||
state.value = "error";
|
||||
errorMessage.value =
|
||||
error.response?.data?.message || "Failed to link Azure subscription";
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const goToDashboard = () => {
|
||||
sessionStorage.removeItem("azure_marketplace_token");
|
||||
router.push({
|
||||
path: "/",
|
||||
query: activatedOrgId.value
|
||||
? { org_identifier: activatedOrgId.value }
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const resetAndRetry = () => {
|
||||
state.value = "select_org";
|
||||
errorMessage.value = "";
|
||||
isProcessing.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
store,
|
||||
state,
|
||||
errorMessage,
|
||||
isProcessing,
|
||||
newOrgName,
|
||||
selectedOrg,
|
||||
eligibleOrganizations,
|
||||
getImageURL,
|
||||
createNewOrgForAzure,
|
||||
linkToExistingOrg,
|
||||
goToDashboard,
|
||||
resetAndRetry,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.azure-marketplace-setup {
|
||||
min-height: 100vh;
|
||||
background: var(--q-background);
|
||||
}
|
||||
|
||||
.appLogo {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.setup-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding-top: 60px;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 != "") {
|
||||
|
||||
Reference in New Issue
Block a user