• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

divviup / divviup-api / 7226558048

15 Dec 2023 08:18PM UTC coverage: 56.347% (+0.03%) from 56.316%
7226558048

push

github

GitHub
upgrade deps and add type: module for new version of vite (#690)

3542 of 6286 relevant lines covered (56.35%)

128.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.43
/src/clients/auth0_client.rs
1
use async_lock::RwLock;
2
use rand::distributions::{Alphanumeric, DistString};
3
use serde::{de::DeserializeOwned, Serialize};
4
use serde_json::{json, Value};
5
use std::{
6
    sync::Arc,
7
    time::{Duration, SystemTime},
8
};
9

10
use trillium::{
11
    Conn,
12
    KnownHeaderName::{Accept, Authorization, ContentType},
13
};
14
use trillium_api::FromConn;
15
use trillium_client::Client;
16
use url::Url;
17

18
use crate::{
19
    clients::{ClientConnExt, ClientError, PostmarkClient},
20
    Config,
21
};
22

23
#[derive(Debug, Clone)]
241✔
24
pub struct Auth0Client {
25
    token: Arc<RwLock<Option<TokenWithExpiry>>>,
26
    client: Client,
27
    base_url: Url,
28
    secret: String,
29
    client_id: String,
30
    postmark_client: PostmarkClient,
31
}
32

33
#[trillium::async_trait]
34
impl FromConn for Auth0Client {
35
    async fn from_conn(conn: &mut Conn) -> Option<Self> {
×
36
        conn.state().cloned()
×
37
    }
×
38
}
39

40
fn generate_password() -> String {
3✔
41
    Alphanumeric.sample_string(&mut rand::thread_rng(), 60)
3✔
42
}
3✔
43

44
impl Auth0Client {
45
    pub fn new(config: &Config) -> Self {
266✔
46
        Self {
266✔
47
            token: Arc::new(RwLock::new(None)),
266✔
48
            client: config.client.clone(),
266✔
49
            base_url: config.auth_url.clone(),
266✔
50
            secret: config.auth_client_secret.clone(),
266✔
51
            client_id: config.auth_client_id.clone(),
266✔
52
            postmark_client: PostmarkClient::new(config),
266✔
53
        }
266✔
54
    }
266✔
55

56
    pub async fn invite(
×
57
        &self,
×
58
        email: &str,
×
59
        account_name: &str,
×
60
    ) -> Result<(String, Url), ClientError> {
×
61
        let user_id = self.create_user(email).await?;
×
62
        let reset = self.password_reset(&user_id).await?;
×
63
        self.postmark_client
×
64
            .send_email_template(
×
65
                email,
×
66
                "user-invitation",
×
67
                &json!({
×
68
                    "email": email,
×
69
                    "account_name": account_name,
×
70
                    "action_url": reset
×
71
                }),
×
72
                None,
×
73
            )
×
74
            .await?;
×
75

76
        Ok((user_id.to_string(), reset))
×
77
    }
×
78

79
    pub async fn password_reset(&self, user_id: &str) -> Result<Url, ClientError> {
2✔
80
        self.post::<Value>(
2✔
81
            "/api/v2/tickets/password-change",
2✔
82
            &json!({ "user_id": user_id, "client_id": &self.client_id }),
2✔
83
        )
2✔
84
        .await?
×
85
        .get("ticket")
2✔
86
        .and_then(Value::as_str)
2✔
87
        .and_then(|u| Url::parse(u).ok())
2✔
88
        .ok_or(ClientError::Other("password reset".to_string()))
2✔
89
    }
2✔
90

91
    pub async fn create_user(&self, email: &str) -> Result<String, ClientError> {
3✔
92
        let user: serde_json::Value = self
3✔
93
            .post(
3✔
94
                "/api/v2/users",
3✔
95
                &json!({
3✔
96
                    "connection": "Username-Password-Authentication",
3✔
97
                    "email": email,
3✔
98
                    "password": generate_password(),
3✔
99
                    "verify_email": false
3✔
100
                }),
3✔
101
            )
3✔
102
            .await?;
5✔
103

104
        user.get("user_id")
3✔
105
            .ok_or_else(|| ClientError::Other("expected user_id".into()))?
3✔
106
            .as_str()
3✔
107
            .ok_or_else(|| ClientError::Other("expected user_id to be a string".into()))
3✔
108
            .map(String::from)
3✔
109
    }
3✔
110

111
    pub async fn users(&self) -> Result<Vec<Value>, ClientError> {
×
112
        self.get("/api/v2/users").await
×
113
    }
×
114

115
    // private below here
116

117
    async fn get_new_token(&self) -> Result<String, ClientError> {
4✔
118
        // we have to check again here because someone may have taken
119
        // a write lock and populated the token since we relinquished
120
        // our read lock
121
        let mut guard = self.token.write().await;
4✔
122
        if let Some(token) = &*guard {
4✔
123
            if token.is_fresh() {
×
124
                return Ok(token.token().to_string());
×
125
            }
×
126
        }
4✔
127

128
        guard.take();
4✔
129

130
        let token = self
4✔
131
            .client
4✔
132
            .post(self.base_url.join("/oauth/token").unwrap())
4✔
133
            .with_header(ContentType, "application/json")
4✔
134
            .with_json_body(&json!({
4✔
135
                "grant_type": "client_credentials",
4✔
136
                "client_id": self.client_id,
4✔
137
                "client_secret": self.secret,
4✔
138
                "audience": self.base_url.join("/api/v2/").unwrap(),
4✔
139
            }))?
4✔
140
            .success_or_client_error()
4✔
141
            .await?
3✔
142
            .response_json::<Token>()
4✔
143
            .await?;
×
144

145
        *guard = Some(token.clone().into());
4✔
146
        Ok(token.access_token)
4✔
147
    }
4✔
148

149
    async fn token(&self) -> Result<String, ClientError> {
5✔
150
        if let Some(token) = &*self.token.read().await {
5✔
151
            if token.is_fresh() {
1✔
152
                return Ok(token.token.clone());
1✔
153
            }
×
154
        }
4✔
155

156
        self.get_new_token().await
4✔
157
    }
5✔
158

159
    async fn post<T>(&self, path: &str, json: &impl Serialize) -> Result<T, ClientError>
5✔
160
    where
5✔
161
        T: DeserializeOwned,
5✔
162
    {
5✔
163
        let token = self.token().await?;
5✔
164
        self.client
5✔
165
            .post(self.base_url.join(path).unwrap())
5✔
166
            .with_header(Accept, "application/json")
5✔
167
            .with_header(ContentType, "application/json")
5✔
168
            .with_header(Authorization, format!("Bearer {token}"))
5✔
169
            .with_json_body(json)?
5✔
170
            .success_or_client_error()
5✔
171
            .await?
2✔
172
            .response_json()
5✔
173
            .await
×
174
            .map_err(ClientError::from)
5✔
175
    }
5✔
176

177
    async fn get<T>(&self, path: &str) -> Result<T, ClientError>
×
178
    where
×
179
        T: DeserializeOwned,
×
180
    {
×
181
        let token = self.token().await?;
×
182
        self.client
×
183
            .get(self.base_url.join(path).unwrap())
×
184
            .with_header(Accept, "application/json")
×
185
            .with_header(Authorization, format!("Bearer {token}"))
×
186
            .success_or_client_error()
×
187
            .await?
×
188
            .response_json()
×
189
            .await
×
190
            .map_err(ClientError::from)
×
191
    }
×
192
}
193

194
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
36✔
195
pub struct Token {
196
    pub access_token: String,
197
    pub expires_in: u64,
198
    pub scope: String,
199
    pub token_type: String,
200
}
201

202
#[derive(Debug, Clone)]
×
203
struct TokenWithExpiry {
204
    token: String,
205
    expires_at: SystemTime,
206
}
207

208
impl TokenWithExpiry {
209
    pub fn is_fresh(&self) -> bool {
1✔
210
        SystemTime::now() < self.expires_at
1✔
211
    }
1✔
212

213
    fn token(&self) -> &str {
×
214
        self.token.as_ref()
×
215
    }
×
216
}
217

218
impl From<Token> for TokenWithExpiry {
219
    fn from(token: Token) -> Self {
4✔
220
        Self {
4✔
221
            token: token.access_token,
4✔
222
            expires_at: SystemTime::now() + Duration::from_secs(token.expires_in),
4✔
223
        }
4✔
224
    }
4✔
225
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc