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#[cfg(test)]
131mod account_tier_tests {
132    use super::*;
133    #[test]
134    fn deser() {
135        assert_eq!(
136            serde_json::from_str::<AccountTier>("\"basic\"").unwrap(),
137            AccountTier::Basic
138        );
139    }
140    #[cfg(feature = "unknown-variants")]
141    #[test]
142    fn unknown_deser() {
143        assert_eq!(
144            serde_json::from_str::<AccountTier>("\"\"").unwrap(),
145            AccountTier::Unknown("".to_string())
146        );
147        assert_eq!(
148            serde_json::from_str::<AccountTier>("\"hisshiss\"").unwrap(),
149            AccountTier::Unknown("hisshiss".to_string())
150        );
151        assert!(serde_json::to_string(&AccountTier::Unknown("asdf".to_string())).is_err());
152    }
153    #[cfg(not(feature = "unknown-variants"))]
154    #[test]
155    fn not_unknown_deser() {
156        assert!(serde_json::from_str::<AccountTier>("\"\"").is_err());
157        assert!(serde_json::from_str::<AccountTier>("\"hisshiss\"").is_err());
158    }
159}
160
161#[derive(Debug, Deserialize, Serialize)]
162#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
163#[typeshare::typeshare]
164pub struct Subscription {
165    pub id: String,
166    pub r#type: SubscriptionType,
167    pub quantity: i32,
168    pub created_at: DateTime<Utc>,
169    pub updated_at: DateTime<Utc>,
170}
171
172#[derive(Debug, Deserialize)]
173#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
174#[typeshare::typeshare]
175pub struct SubscriptionRequest {
176    pub id: String,
177    pub r#type: SubscriptionType,
178    pub quantity: i32,
179}
180
181#[derive(
182    // std
183    Clone,
184    Debug,
185    Eq,
186    PartialEq,
187    // serde
188    Deserialize,
189    Serialize,
190    // strum
191    EnumString,
192    strum::Display,
193    IntoStaticStr,
194)]
195#[serde(rename_all = "lowercase")]
196#[strum(serialize_all = "lowercase")]
197#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
198#[typeshare::typeshare]
199pub enum SubscriptionType {
200    Pro,
201    Rds,
202
203    /// Forward compatibility
204    #[cfg(feature = "unknown-variants")]
205    #[doc(hidden)]
206    #[typeshare(skip)]
207    #[serde(untagged, skip_serializing)]
208    #[strum(default, to_string = "Unknown: {0}")]
209    Unknown(String),
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}
288
289#[derive(Clone, Debug, Deserialize, Serialize)]
290#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
291#[typeshare::typeshare]
292pub struct AccountLimitsResponse {
293    /// Number of projects the user currently has
294    pub projects_count: Option<u32>,
295    /// Number of projects the user may have total
296    pub projects_limit: Option<u32>,
297    /// Number of active projects the user currently has
298    pub active_projects_count: Option<u32>,
299    /// Number of projects the user may have active at once
300    pub active_projects_limit: Option<u32>,
301    /// Number of custom domains the user currently has
302    pub certificate_count: Option<u32>,
303    /// Number of custom domains the user may have total
304    pub certificate_limit: Option<u32>,
305}