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?)
}
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() {
let mut req = UpdateUserRequest::new();
req.name = Some(name.to_string());
self.update_user(&existing_user.user_id, req).await?
} else {
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?)
}
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(())
}
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")))
}
}
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();
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")
}