Skip to main content

auth0_integration/services/
auth0_client.rs

1use rand::Rng;
2use rand::seq::SliceRandom;
3use serde::Serialize;
4
5use crate::config::Auth0Config;
6use crate::error::AppError;
7use crate::models::{Auth0Role, Auth0User, CreateUserRequest, Role, UpdateUserRequest};
8use crate::services::HttpClient;
9
10pub struct Auth0Client {
11    http: HttpClient,
12    token: String,
13}
14
15impl Auth0Client {
16    pub fn new(config: &Auth0Config, token: String) -> Self {
17        Self {
18            http: HttpClient::new(config),
19            token,
20        }
21    }
22
23    pub async fn get_user_by_email(&self, email: &str) -> Result<Vec<Auth0User>, AppError> {
24        let path = format!("/api/v2/users-by-email?email={}", email);
25        let res = self.http.get_authorized(&path, &self.token).await?;
26
27        if !res.status().is_success() {
28            let text = res.text().await.unwrap_or_default();
29            return Err(AppError::Auth0(text));
30        }
31
32        Ok(res.json::<Vec<Auth0User>>().await?)
33    }
34
35    /// Creates a new user in Auth0 with the given role.
36    ///
37    /// Returns an error if creation fails.
38    ///
39    /// `role` accepts values like `"admin"`, `"super_admin"`, or `"worker"` (mapped to `"ADMIN"`,
40    /// `"SUPER_ADMIN"`, `"WORKER"` in Auth0).
41    pub async fn create_user(
42        &self,
43        name: &str,
44        email: &str,
45        role: &Role,
46    ) -> Result<Auth0User, AppError> {
47        let password = generate_password();
48        let body = CreateUserRequest::new("Username-Password-Authentication", email, name, password);
49        let res = self
50            .http
51            .post_authorized("/api/v2/users", &body, &self.token)
52            .await?;
53
54        if !res.status().is_success() {
55            let text = res.text().await.unwrap_or_default();
56            return Err(AppError::Auth0(format!("Failed to create user: {text}")));
57        }
58
59        let user = res.json::<Auth0User>().await?;
60        self.assign_role(&user.user_id, role).await?;
61
62        Ok(user)
63    }
64
65    pub async fn update_user(
66        &self,
67        user_id: &str,
68        req: UpdateUserRequest,
69        role: Option<&Role>,
70    ) -> Result<Auth0User, AppError> {
71        let path = format!("/api/v2/users/{}", user_id);
72        let res = self
73            .http
74            .patch_authorized(&path, &req, &self.token)
75            .await?;
76
77        if !res.status().is_success() {
78            let text = res.text().await.unwrap_or_default();
79            return Err(AppError::Auth0(text));
80        }
81
82        let user = res.json::<Auth0User>().await?;
83
84        if let Some(role) = role {
85            let current_roles = self.get_user_roles(user_id).await?;
86            let already_has_role = current_roles
87                .iter()
88                .any(|r| r.name == role.as_auth0_name());
89            if !already_has_role {
90                self.assign_role(user_id, role).await?;
91            }
92        }
93
94        Ok(user)
95    }
96
97    pub async fn delete_user(&self, user_id: &str) -> Result<(), AppError> {
98        let path = format!("/api/v2/users/{}", user_id);
99        let res = self
100            .http
101            .delete_authorized(&path, &self.token)
102            .await?;
103
104        if !res.status().is_success() {
105            let text = res.text().await.unwrap_or_default();
106            return Err(AppError::Auth0(format!("Failed to delete user: {text}")));
107        }
108
109        Ok(())
110    }
111
112    /// Replaces all existing roles on the user with the given role.
113    /// Any roles the user already has are removed first.
114    async fn assign_role(&self, user_id: &str, role: &Role) -> Result<(), AppError> {
115        let role_id = self.get_role_id(role).await?;
116        let path = format!("/api/v2/users/{}/roles", user_id);
117
118        #[derive(Serialize)]
119        struct Body {
120            roles: Vec<String>,
121        }
122
123        // Remove existing roles first
124        let existing = self.get_user_roles(user_id).await?;
125        if !existing.is_empty() {
126            let existing_ids: Vec<String> = existing.into_iter().map(|r| r.id).collect();
127            let res = self
128                .http
129                .delete_authorized_with_body(&path, &Body { roles: existing_ids }, &self.token)
130                .await?;
131            if !res.status().is_success() {
132                let text = res.text().await.unwrap_or_default();
133                return Err(AppError::Auth0(format!("Failed to remove existing roles: {text}")));
134            }
135        }
136
137        let res = self
138            .http
139            .post_authorized(&path, &Body { roles: vec![role_id] }, &self.token)
140            .await?;
141
142        if !res.status().is_success() {
143            let text = res.text().await.unwrap_or_default();
144            return Err(AppError::Auth0(format!("Failed to assign role: {text}")));
145        }
146
147        Ok(())
148    }
149
150    async fn get_user_roles(&self, user_id: &str) -> Result<Vec<Auth0Role>, AppError> {
151        let path = format!("/api/v2/users/{}/roles", user_id);
152        let res = self.http.get_authorized(&path, &self.token).await?;
153
154        if !res.status().is_success() {
155            let text = res.text().await.unwrap_or_default();
156            return Err(AppError::Auth0(format!("Failed to fetch user roles: {text}")));
157        }
158
159        Ok(res.json::<Vec<Auth0Role>>().await?)
160    }
161
162    /// Fetches all Auth0 roles and returns the ID matching `role.as_auth0_name()`.
163    async fn get_role_id(&self, role: &Role) -> Result<String, AppError> {
164        let auth0_name = role.as_auth0_name();
165        let path = format!("/api/v2/roles?name_filter={}", auth0_name);
166        let res = self.http.get_authorized(&path, &self.token).await?;
167
168        if !res.status().is_success() {
169            let text = res.text().await.unwrap_or_default();
170            return Err(AppError::Auth0(format!("Failed to fetch roles: {text}")));
171        }
172
173        let roles = res.json::<Vec<Auth0Role>>().await?;
174        roles
175            .into_iter()
176            .find(|r| r.name == auth0_name)
177            .map(|r| r.id)
178            .ok_or_else(|| AppError::Auth0(format!("Role '{auth0_name}' not found in Auth0")))
179    }
180}
181
182/// Generates a cryptographically adequate random password that satisfies typical Auth0 policies:
183/// at least one uppercase, one lowercase, one digit, one special character; 16 chars total.
184fn generate_password() -> String {
185    let mut rng = rand::thread_rng();
186
187    let uppercase: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ";
188    let lowercase: &[u8] = b"abcdefghjkmnpqrstuvwxyz";
189    let digits: &[u8] = b"23456789";
190    let special: &[u8] = b"!@#$%^&*";
191    let all: Vec<u8> = [uppercase, lowercase, digits, special].concat();
192
193    // Guarantee at least one of each required class
194    let mut chars: Vec<u8> = vec![
195        uppercase[rng.gen_range(0..uppercase.len())],
196        lowercase[rng.gen_range(0..lowercase.len())],
197        digits[rng.gen_range(0..digits.len())],
198        special[rng.gen_range(0..special.len())],
199    ];
200
201    for _ in 0..12 {
202        chars.push(all[rng.gen_range(0..all.len())]);
203    }
204
205    chars.shuffle(&mut rng);
206    String::from_utf8(chars).expect("password chars are all ASCII")
207}