feldera_cloud1_client/
license.rs

1//! License related types exchanged between license server and feldera platform.
2use chrono::{DateTime, Utc};
3use reqwest::StatusCode;
4use serde::{Deserialize, Serialize};
5use std::time::Instant;
6use thiserror::Error as ThisError;
7use utoipa::ToSchema;
8
9use crate::source_error;
10
11/// Request to verify a license.
12/// Shared type between client and server.
13#[derive(Serialize, Deserialize, Debug)]
14pub struct LicenseCheckRequest {
15    pub account_id: String,
16    pub license_key: String,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
20#[allow(dead_code)]
21pub enum DisplaySchedule {
22    /// Display it only once: after dismissal do not show it again
23    Once,
24    /// Display it again the next session if it is dismissed
25    Session,
26    /// Display it again after a certain period of time after it is dismissed
27    Every { seconds: u64 },
28    /// Always display it, do not allow it to be dismissed
29    Always,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
33pub struct LicenseInformation {
34    /// Timestamp when the server responded.
35    pub current: DateTime<Utc>,
36    /// Timestamp at which point the license expires
37    pub valid_until: Option<DateTime<Utc>>,
38    /// Whether the license is a trial
39    pub is_trial: bool,
40    /// Optional description of the advantages of extending the license / upgrading from a trial
41    pub description_html: String,
42    /// URL that navigates the user to extend / upgrade their license
43    pub extension_url: Option<String>,
44    /// Timestamp from which the user should be reminded of the license expiring soon
45    pub remind_starting_at: Option<DateTime<Utc>>,
46    /// Suggested frequency of reminding the user about the license expiring soon
47    pub remind_schedule: DisplaySchedule,
48}
49
50/// Enumeration of the errors when trying to check whether license exists or not.
51#[derive(ThisError, Debug, Clone, PartialEq)]
52pub enum LicenseRetrievalError {
53    #[error("failed to serialize request to JSON due to: {error}")]
54    SerializeRequestToJsonFailed { error: String },
55    #[error("failed to send request due to: {error}")]
56    SendRequestFailed { error: String },
57    #[error("license information returned could not be deserialized due to: {error}")]
58    JsonDeserializeFailed { error: String },
59    #[error("server indicated the service is currently unavailable (503)")]
60    ServiceUnavailable,
61    #[error("server responded with an unexpected HTTP status code ({0})")]
62    UnexpectedResponseStatusCode(StatusCode),
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub enum LicenseRetrievalResult {
67    /// License with that license key exists and thus the server retrieved information about it.
68    /// The information contains whether the license is expired or not.
69    Exists(LicenseInformation),
70
71    /// License with that license key does not exist according to the server.
72    /// This means the license key is invalid.
73    DoesNotExistInvalid,
74
75    /// Unable to check whether the license exists at this moment.
76    /// It should be retried in the future.
77    Unable(LicenseRetrievalError),
78}
79
80/// Verifies the license with the API endpoint.
81pub async fn retrieve_license(
82    // Cloud API endpoint
83    cloud_api_endpoint: &str,
84    // License key
85    license_key: &str,
86) -> (Instant, LicenseRetrievalResult) {
87    let endpoint_license_retrieval = format!("{cloud_api_endpoint}/license");
88    let client = reqwest::Client::new();
89    let result = client
90        .get(&endpoint_license_retrieval)
91        .header(
92            reqwest::header::AUTHORIZATION,
93            format!("Bearer {}", license_key),
94        )
95        .send()
96        .await;
97    let now = Instant::now();
98    let result = match result {
99        Ok(response) => {
100            let status_code = response.status();
101            if status_code == StatusCode::OK {
102                match response.json::<LicenseInformation>().await {
103                    Ok(license_information) => LicenseRetrievalResult::Exists(license_information),
104                    Err(error) => LicenseRetrievalResult::Unable(
105                        LicenseRetrievalError::JsonDeserializeFailed {
106                            error: error.to_string(),
107                        },
108                    ),
109                }
110            } else if status_code == StatusCode::FORBIDDEN {
111                LicenseRetrievalResult::DoesNotExistInvalid
112            } else if status_code == StatusCode::SERVICE_UNAVAILABLE {
113                LicenseRetrievalResult::Unable(LicenseRetrievalError::ServiceUnavailable)
114            } else {
115                LicenseRetrievalResult::Unable(LicenseRetrievalError::UnexpectedResponseStatusCode(
116                    status_code,
117                ))
118            }
119        }
120        Err(e) => {
121            let source_err = source_error(&e);
122            let error = format!("{e}, source: {source_err}");
123            LicenseRetrievalResult::Unable(LicenseRetrievalError::SendRequestFailed { error })
124        }
125    };
126    (now, result)
127}