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    /// Gets or creates a user in Auth0, then assigns the given role.
36    ///
37    /// - If the user **already exists** (looked up by email): updates their profile (name) and assigns the role.
38    /// - If the user **does not exist**: creates them with a random secure password and assigns the role.
39    ///
40    /// `role` accepts values like `"admin"`, `"super_admin"`, or `"worker"` (mapped to `"ADMIN"`,
41    /// `"SUPER_ADMIN"`, `"WORKER"` in Auth0).
42    pub async fn get_or_create_user(
43        &self,
44        name: &str,
45        email: &str,
46        role: &Role,
47    ) -> Result<Auth0User, AppError> {
48        let existing = self.get_user_by_email(email).await?;
49
50        let user = if let Some(existing_user) = existing.into_iter().next() {
51            // User exists — update non-email fields
52            let mut req = UpdateUserRequest::new();
53            req.name = Some(name.to_string());
54            self.update_user(&existing_user.user_id, req).await?
55        } else {
56            // User does not exist — create with a random password
57            let password = generate_password();
58            let body = CreateUserRequest::new("Username-Password-Authentication", email, name, password);
59            let res = self
60                .http
61                .post_authorized("/api/v2/users", &body, &self.token)
62                .await?;
63
64            if !res.status().is_success() {
65                let text = res.text().await.unwrap_or_default();
66                return Err(AppError::Auth0(format!("Failed to create user: {text}")));
67            }
68
69            res.json::<Auth0User>().await?
70        };
71
72        self.assign_role(&user.user_id, role).await?;
73
74        Ok(user)
75    }
76
77    pub async fn update_user(&self, user_id: &str, req: UpdateUserRequest) -> Result<Auth0User, AppError> {
78        let path = format!("/api/v2/users/{}", user_id);
79        let res = self
80            .http
81            .patch_authorized(&path, &req, &self.token)
82            .await?;
83
84        if !res.status().is_success() {
85            let text = res.text().await.unwrap_or_default();
86            return Err(AppError::Auth0(text));
87        }
88
89        Ok(res.json::<Auth0User>().await?)
90    }
91
92    /// Looks up the Auth0 role ID by name and assigns it to the user.
93    async fn assign_role(&self, user_id: &str, role: &Role) -> Result<(), AppError> {
94        let role_id = self.get_role_id(role).await?;
95        let path = format!("/api/v2/users/{}/roles", user_id);
96
97        #[derive(Serialize)]
98        struct Body {
99            roles: Vec<String>,
100        }
101
102        let res = self
103            .http
104            .post_authorized(&path, &Body { roles: vec![role_id] }, &self.token)
105            .await?;
106
107        if !res.status().is_success() {
108            let text = res.text().await.unwrap_or_default();
109            return Err(AppError::Auth0(format!("Failed to assign role: {text}")));
110        }
111
112        Ok(())
113    }
114
115    /// Fetches all Auth0 roles and returns the ID matching `role.as_auth0_name()`.
116    async fn get_role_id(&self, role: &Role) -> Result<String, AppError> {
117        let auth0_name = role.as_auth0_name();
118        let path = format!("/api/v2/roles?name_filter={}", auth0_name);
119        let res = self.http.get_authorized(&path, &self.token).await?;
120
121        if !res.status().is_success() {
122            let text = res.text().await.unwrap_or_default();
123            return Err(AppError::Auth0(format!("Failed to fetch roles: {text}")));
124        }
125
126        let roles = res.json::<Vec<Auth0Role>>().await?;
127        roles
128            .into_iter()
129            .find(|r| r.name == auth0_name)
130            .map(|r| r.id)
131            .ok_or_else(|| AppError::Auth0(format!("Role '{auth0_name}' not found in Auth0")))
132    }
133}
134
135/// Generates a cryptographically adequate random password that satisfies typical Auth0 policies:
136/// at least one uppercase, one lowercase, one digit, one special character; 16 chars total.
137fn generate_password() -> String {
138    let mut rng = rand::thread_rng();
139
140    let uppercase: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ";
141    let lowercase: &[u8] = b"abcdefghjkmnpqrstuvwxyz";
142    let digits: &[u8] = b"23456789";
143    let special: &[u8] = b"!@#$%^&*";
144    let all: Vec<u8> = [uppercase, lowercase, digits, special].concat();
145
146    // Guarantee at least one of each required class
147    let mut chars: Vec<u8> = vec![
148        uppercase[rng.gen_range(0..uppercase.len())],
149        lowercase[rng.gen_range(0..lowercase.len())],
150        digits[rng.gen_range(0..digits.len())],
151        special[rng.gen_range(0..special.len())],
152    ];
153
154    for _ in 0..12 {
155        chars.push(all[rng.gen_range(0..all.len())]);
156    }
157
158    chars.shuffle(&mut rng);
159    String::from_utf8(chars).expect("password chars are all ASCII")
160}