shuttle_common/models/
user.rs

1use std::collections::HashMap;
2#[cfg(feature = "display")]
3use std::fmt::Write;
4
5use chrono::{DateTime, NaiveDate, Utc};
6#[cfg(feature = "display")]
7use crossterm::style::Stylize;
8use serde::{Deserialize, Serialize};
9use strum::{EnumString, IntoStaticStr};
10
11use super::project::ProjectUsageResponse;
12
13#[derive(Debug, Deserialize, Serialize)]
14#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
15#[typeshare::typeshare]
16pub struct UserResponse {
17    pub id: String,
18    /// Auth0 id
19    pub auth0_id: Option<String>,
20    pub created_at: DateTime<Utc>,
21    // deprecated
22    pub key: Option<String>,
23    pub account_tier: AccountTier,
24    pub subscriptions: Option<Vec<Subscription>>,
25    pub flags: Option<Vec<String>>,
26}
27
28impl UserResponse {
29    #[cfg(feature = "display")]
30    pub fn to_string_colored(&self) -> String {
31        let mut s = String::new();
32        writeln!(&mut s, "{}", "Account info:".bold()).unwrap();
33        writeln!(&mut s, "  User ID: {}", self.id).unwrap();
34        writeln!(
35            &mut s,
36            "  Account tier: {}",
37            self.account_tier.to_string_fancy()
38        )
39        .unwrap();
40        if let Some(subs) = self.subscriptions.as_ref() {
41            if !subs.is_empty() {
42                writeln!(&mut s, "  Subscriptions:").unwrap();
43                for sub in subs {
44                    writeln!(
45                        &mut s,
46                        "    - {}: Type: {}, Quantity: {}, Created: {}, Updated: {}",
47                        sub.id, sub.r#type, sub.quantity, sub.created_at, sub.updated_at,
48                    )
49                    .unwrap();
50                }
51            }
52        }
53        if let Some(flags) = self.flags.as_ref() {
54            if !flags.is_empty() {
55                writeln!(&mut s, "  Feature flags:").unwrap();
56                for flag in flags {
57                    writeln!(&mut s, "    - {}", flag).unwrap();
58                }
59            }
60        }
61
62        s
63    }
64}
65
66#[derive(
67    // std
68    Clone,
69    Debug,
70    Default,
71    Eq,
72    PartialEq,
73    Ord,
74    PartialOrd,
75    // serde
76    Deserialize,
77    Serialize,
78    // strum
79    EnumString,
80    IntoStaticStr,
81    strum::Display,
82)]
83#[serde(rename_all = "lowercase")]
84#[strum(serialize_all = "lowercase")]
85#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
86#[typeshare::typeshare]
87pub enum AccountTier {
88    #[default]
89    Basic,
90    /// Partial access to Pro features and higher limits than Basic
91    ProTrial,
92    /// A Basic user that is pending a payment to go back to Pro
93    // soft-deprecated
94    PendingPaymentPro,
95    /// Pro user with an expiring subscription
96    // soft-deprecated
97    CancelledPro,
98    Pro,
99    Growth,
100    /// Growth tier but even higher limits
101    Employee,
102    /// No limits, full API access, admin endpoint access
103    Admin,
104
105    /// Forward compatibility
106    #[cfg(feature = "unknown-variants")]
107    #[doc(hidden)]
108    #[typeshare(skip)]
109    #[serde(untagged, skip_serializing)]
110    #[strum(default, to_string = "Unknown: {0}")]
111    Unknown(String),
112}
113impl AccountTier {
114    pub fn to_string_fancy(&self) -> String {
115        match self {
116            Self::Basic => "Community".to_owned(),
117            Self::ProTrial => "Pro Trial".to_owned(),
118            Self::PendingPaymentPro => "Community (pending payment for Pro)".to_owned(),
119            Self::CancelledPro => "Pro (subscription cancelled)".to_owned(),
120            Self::Pro => "Pro".to_owned(),
121            Self::Growth => "Growth".to_owned(),
122            Self::Employee => "Employee".to_owned(),
123            Self::Admin => "Admin".to_owned(),
124            #[cfg(feature = "unknown-variants")]
125            Self::Unknown(_) => self.to_string(),
126        }
127    }
128}
129
130#[derive(Debug, Deserialize, Serialize)]
131#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
132#[typeshare::typeshare]
133pub struct Subscription {
134    pub id: String,
135    pub r#type: SubscriptionType,
136    pub quantity: i32,
137    pub created_at: DateTime<Utc>,
138    pub updated_at: DateTime<Utc>,
139}
140
141#[derive(Debug, Deserialize)]
142#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
143#[typeshare::typeshare]
144pub struct SubscriptionRequest {
145    pub id: String,
146    pub r#type: SubscriptionType,
147    pub quantity: i32,
148}
149
150#[derive(
151    // std
152    Clone,
153    Debug,
154    Eq,
155    PartialEq,
156    // serde
157    Deserialize,
158    Serialize,
159    // strum
160    EnumString,
161    strum::Display,
162    IntoStaticStr,
163)]
164#[serde(rename_all = "lowercase")]
165#[strum(serialize_all = "lowercase")]
166#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
167#[typeshare::typeshare]
168pub enum SubscriptionType {
169    Pro,
170    Rds,
171
172    /// Forward compatibility
173    #[cfg(feature = "unknown-variants")]
174    #[doc(hidden)]
175    #[typeshare(skip)]
176    #[serde(untagged, skip_serializing)]
177    #[strum(default, to_string = "Unknown: {0}")]
178    Unknown(String),
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    #[test]
185    fn deser() {
186        assert_eq!(
187            serde_json::from_str::<AccountTier>("\"basic\"").unwrap(),
188            AccountTier::Basic
189        );
190    }
191    #[cfg(feature = "unknown-variants")]
192    #[test]
193    fn unknown_deser() {
194        assert_eq!(
195            serde_json::from_str::<AccountTier>("\"\"").unwrap(),
196            AccountTier::Unknown("".to_string())
197        );
198        assert_eq!(
199            serde_json::from_str::<AccountTier>("\"hisshiss\"").unwrap(),
200            AccountTier::Unknown("hisshiss".to_string())
201        );
202        assert!(serde_json::to_string(&AccountTier::Unknown("asdf".to_string())).is_err());
203    }
204    #[cfg(not(feature = "unknown-variants"))]
205    #[test]
206    fn not_unknown_deser() {
207        assert!(serde_json::from_str::<AccountTier>("\"\"").is_err());
208        assert!(serde_json::from_str::<AccountTier>("\"hisshiss\"").is_err());
209    }
210}
211
212#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
213#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
214#[typeshare::typeshare]
215pub struct CreateAccountRequest {
216    pub auth0_id: String,
217    pub account_tier: AccountTier,
218}
219
220#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
221#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
222#[typeshare::typeshare]
223pub struct UpdateAccountTierRequest {
224    pub account_tier: AccountTier,
225}
226
227/// Sub-Response for the /user/me/usage backend endpoint
228#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
229#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
230#[typeshare::typeshare]
231pub struct UserBillingCycle {
232    /// Billing cycle start, or monthly from user creation
233    /// depending on the account tier
234    pub start: NaiveDate,
235
236    /// Billing cycle end, or end of month from user creation
237    /// depending on the account tier
238    pub end: NaiveDate,
239}
240
241#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
242#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
243#[typeshare::typeshare]
244pub struct UserUsageCustomDomains {
245    pub used: u32,
246    pub limit: u32,
247}
248
249#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
250#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
251#[typeshare::typeshare]
252pub struct UserUsageProjects {
253    pub used: u32,
254    pub limit: u32,
255}
256
257#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)]
258#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
259#[typeshare::typeshare]
260pub struct UserUsageTeamMembers {
261    pub used: u32,
262    pub limit: u32,
263}
264
265#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
266#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
267#[typeshare::typeshare]
268pub struct UserOverviewResponse {
269    pub custom_domains: UserUsageCustomDomains,
270    pub projects: UserUsageProjects,
271    pub team_members: Option<UserUsageTeamMembers>,
272}
273
274/// Response for the /user/me/usage backend endpoint
275#[derive(Debug, Default, Deserialize, Serialize, Clone)]
276#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
277#[typeshare::typeshare]
278pub struct UserUsageResponse {
279    /// Billing cycle for user, will be None if no usage data exists for user.
280    pub billing_cycle: Option<UserBillingCycle>,
281
282    /// User overview information including project and domain counts
283    pub user: Option<UserOverviewResponse>,
284    /// HashMap of project related metrics for this cycle keyed by project_id. Will be empty
285    /// if no project usage data exists for user.
286    pub projects: HashMap<String, ProjectUsageResponse>,
287}