auth0-integration 0.6.1

Auth0 client library for M2M token retrieval and JWT validation (RS256)
Documentation
use rand::Rng;
use rand::seq::SliceRandom;
use serde::Serialize;

use crate::config::Auth0Config;
use crate::error::AppError;
use crate::models::{Auth0Role, Auth0User, CreateUserRequest, Role, UpdateUserRequest};
use crate::services::HttpClient;

pub struct Auth0Client {
    http: HttpClient,
    token: String,
}

impl Auth0Client {
    pub fn new(config: &Auth0Config, token: String) -> Self {
        Self {
            http: HttpClient::new(config),
            token,
        }
    }

    pub async fn get_user_by_email(&self, email: &str) -> Result<Vec<Auth0User>, AppError> {
        let path = format!("/api/v2/users-by-email?email={}", email);
        let res = self.http.get_authorized(&path, &self.token).await?;

        if !res.status().is_success() {
            let text = res.text().await.unwrap_or_default();
            return Err(AppError::Auth0(text));
        }

        Ok(res.json::<Vec<Auth0User>>().await?)
    }

    /// Gets or creates a user in Auth0, then assigns the given role.
    ///
    /// - If the user **already exists** (looked up by email): updates their profile (name) and assigns the role.
    /// - If the user **does not exist**: creates them with a random secure password and assigns the role.
    ///
    /// `role` accepts values like `"admin"`, `"super_admin"`, or `"worker"` (mapped to `"ADMIN"`,
    /// `"SUPER_ADMIN"`, `"WORKER"` in Auth0).
    pub async fn create_or_update_user(
        &self,
        name: &str,
        email: &str,
        role: &Role,
    ) -> Result<Auth0User, AppError> {
        let existing = self.get_user_by_email(email).await?;

        let user = if let Some(existing_user) = existing.into_iter().next() {
            // User exists — update non-email fields
            let mut req = UpdateUserRequest::new();
            req.name = Some(name.to_string());
            self.update_user(&existing_user.user_id, req).await?
        } else {
            // User does not exist — create with a random password
            let password = generate_password();
            let body = CreateUserRequest::new("Username-Password-Authentication", email, name, password);
            let res = self
                .http
                .post_authorized("/api/v2/users", &body, &self.token)
                .await?;

            if !res.status().is_success() {
                let text = res.text().await.unwrap_or_default();
                return Err(AppError::Auth0(format!("Failed to create user: {text}")));
            }

            res.json::<Auth0User>().await?
        };

        self.assign_role(&user.user_id, role).await?;

        Ok(user)
    }

    pub async fn update_user(&self, user_id: &str, req: UpdateUserRequest) -> Result<Auth0User, AppError> {
        let path = format!("/api/v2/users/{}", user_id);
        let res = self
            .http
            .patch_authorized(&path, &req, &self.token)
            .await?;

        if !res.status().is_success() {
            let text = res.text().await.unwrap_or_default();
            return Err(AppError::Auth0(text));
        }

        Ok(res.json::<Auth0User>().await?)
    }

    /// Looks up the Auth0 role ID by name and assigns it to the user.
    async fn assign_role(&self, user_id: &str, role: &Role) -> Result<(), AppError> {
        let role_id = self.get_role_id(role).await?;
        let path = format!("/api/v2/users/{}/roles", user_id);

        #[derive(Serialize)]
        struct Body {
            roles: Vec<String>,
        }

        let res = self
            .http
            .post_authorized(&path, &Body { roles: vec![role_id] }, &self.token)
            .await?;

        if !res.status().is_success() {
            let text = res.text().await.unwrap_or_default();
            return Err(AppError::Auth0(format!("Failed to assign role: {text}")));
        }

        Ok(())
    }

    /// Fetches all Auth0 roles and returns the ID matching `role.as_auth0_name()`.
    async fn get_role_id(&self, role: &Role) -> Result<String, AppError> {
        let auth0_name = role.as_auth0_name();
        let path = format!("/api/v2/roles?name_filter={}", auth0_name);
        let res = self.http.get_authorized(&path, &self.token).await?;

        if !res.status().is_success() {
            let text = res.text().await.unwrap_or_default();
            return Err(AppError::Auth0(format!("Failed to fetch roles: {text}")));
        }

        let roles = res.json::<Vec<Auth0Role>>().await?;
        roles
            .into_iter()
            .find(|r| r.name == auth0_name)
            .map(|r| r.id)
            .ok_or_else(|| AppError::Auth0(format!("Role '{auth0_name}' not found in Auth0")))
    }
}

/// Generates a cryptographically adequate random password that satisfies typical Auth0 policies:
/// at least one uppercase, one lowercase, one digit, one special character; 16 chars total.
fn generate_password() -> String {
    let mut rng = rand::thread_rng();

    let uppercase: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ";
    let lowercase: &[u8] = b"abcdefghjkmnpqrstuvwxyz";
    let digits: &[u8] = b"23456789";
    let special: &[u8] = b"!@#$%^&*";
    let all: Vec<u8> = [uppercase, lowercase, digits, special].concat();

    // Guarantee at least one of each required class
    let mut chars: Vec<u8> = vec![
        uppercase[rng.gen_range(0..uppercase.len())],
        lowercase[rng.gen_range(0..lowercase.len())],
        digits[rng.gen_range(0..digits.len())],
        special[rng.gen_range(0..special.len())],
    ];

    for _ in 0..12 {
        chars.push(all[rng.gen_range(0..all.len())]);
    }

    chars.shuffle(&mut rng);
    String::from_utf8(chars).expect("password chars are all ASCII")
}