use crate::client::{AuraClient, RequestBody};
use crate::error::AuraError;
use crate::types::*;
pub const OAUTH_FORWARDABLE_QUERY_PARAMS: [&str; 10] = [
"access_type",
"acr_values",
"claims_locales",
"display",
"hd",
"include_granted_scopes",
"login_hint",
"max_age",
"prompt",
"ui_locales",
];
pub const OAUTH_SDK_OWNED_QUERY_PARAMS: [&str; 6] = [
"code_challenge",
"code_challenge_method",
"project_id",
"redirect_to",
"redirect_uri",
"scopes",
];
pub struct AuthService {
client: AuraClient,
}
impl AuthService {
pub fn new(client: AuraClient) -> Self {
Self { client }
}
fn prefix(&self) -> String {
"/v1/auth".to_string()
}
pub fn on_auth_state_change(
&self,
) -> tokio::sync::broadcast::Receiver<(Option<String>, Option<User>)> {
self.client.auth_store().subscribe()
}
pub async fn sign_up(
&self,
req: SignUpRequest,
) -> Result<AuraResponse<LoginResponse>, AuraError> {
let res = self
.client
.request::<LoginResponse>(
reqwest::Method::POST,
&format!("{}/register", self.prefix()),
RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
)
.await?;
if let Some(LoginResponse::TokenPair(ref tp)) = res.data {
self.client.auth_store().save(
tp.access_token.clone(),
tp.refresh_token.clone(),
tp.user.clone(),
);
}
Ok(res)
}
pub async fn sign_in_with_password(
&self,
req: SignInRequest,
) -> Result<AuraResponse<LoginResponse>, AuraError> {
let res = self
.client
.request::<LoginResponse>(
reqwest::Method::POST,
&format!("{}/login", self.prefix()),
RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
)
.await?;
if let Some(LoginResponse::TokenPair(ref tp)) = res.data {
self.client.auth_store().save(
tp.access_token.clone(),
tp.refresh_token.clone(),
tp.user.clone(),
);
}
Ok(res)
}
#[allow(clippy::result_large_err)]
pub fn sign_in_with_oauth(
&self,
provider: &str,
options: Option<OAuthSignInOptions>,
) -> Result<String, AuraError> {
let opts = options.unwrap_or_default();
let mut params: Vec<(String, String)> = Vec::new();
if let Some(ref redirect_to) = opts.redirect_to {
params.push(("redirect_to".to_string(), redirect_to.clone()));
}
if let Some(ref scopes) = opts.scopes {
params.push(("scopes".to_string(), scopes.clone()));
}
if let Some(ref qp) = opts.query_params {
let mut cles: Vec<&String> = qp.keys().collect();
cles.sort();
for k in cles {
let v = &qp[k];
if OAUTH_FORWARDABLE_QUERY_PARAMS.contains(&k.as_str()) {
params.push((k.clone(), v.clone()));
continue;
}
let raison = if OAUTH_SDK_OWNED_QUERY_PARAMS.contains(&k.as_str()) {
let dediee = match k.as_str() {
"redirect_uri" | "redirect_to" => "redirect_to",
"scopes" => "scopes",
"project_id" => "l'URL du projet passée à create_client()",
_ => "les options PKCE dédiées",
};
format!(
"il est construit par sign_in_with_oauth (utilisez l'option dédiée : \
{dediee})"
)
} else {
format!(
"il n'est pas transmis au fournisseur. Seuls ces paramètres \
d'expérience de connexion le sont : {}",
OAUTH_FORWARDABLE_QUERY_PARAMS.join(", ")
)
};
return Err(AuraError::new(
400,
"invalid_oauth_query_param",
&format!("sign_in_with_oauth: query_params.{k} refusé — {raison}."),
None,
));
}
}
let query = params
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&");
Ok(format!(
"{}/v1/auth/oauth/{}/start?{}",
self.client.base_url(),
urlencoding::encode(provider),
query
))
}
pub async fn sign_in_with_magic_link(
&self,
req: MagicLinkRequest,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::POST,
&format!("{}/magic-link", self.prefix()),
RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
)
.await
}
pub async fn sign_in_anonymously(&self) -> Result<AuraResponse<TokenPair>, AuraError> {
let res = self
.client
.request::<TokenPair>(
reqwest::Method::POST,
&format!("{}/anonymous", self.prefix()),
RequestBody::None,
)
.await?;
if let Some(ref tp) = res.data {
self.client.auth_store().save(
tp.access_token.clone(),
tp.refresh_token.clone(),
tp.user.clone(),
);
}
Ok(res)
}
pub async fn sign_out(&self) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let refresh_token = self.client.auth_store().refresh_token();
let body = serde_json::json!({
"refresh_token": refresh_token,
});
let res = self
.client
.request(
reqwest::Method::POST,
&format!("{}/logout", self.prefix()),
RequestBody::Json(body),
)
.await;
self.client.auth_store().clear();
res
}
pub async fn refresh_session(&self) -> Result<AuraResponse<TokenPair>, AuraError> {
let refresh_token = self.client.auth_store().refresh_token();
let body = serde_json::json!({
"refresh_token": refresh_token,
});
let res = self
.client
.request::<TokenPair>(
reqwest::Method::POST,
&format!("{}/refresh", self.prefix()),
RequestBody::Json(body),
)
.await?;
if let Some(ref tp) = res.data {
self.client.auth_store().save(
tp.access_token.clone(),
tp.refresh_token.clone(),
tp.user.clone(),
);
}
Ok(res)
}
pub fn get_session(&self) -> (Option<String>, Option<User>) {
(
self.client.auth_store().token(),
self.client.auth_store().user(),
)
}
pub async fn list_sessions(&self) -> Result<AuraResponse<Vec<SessionInfo>>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/sessions", self.prefix()),
RequestBody::None,
)
.await
}
pub async fn revoke_session(
&self,
session_id: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::DELETE,
&format!("{}/sessions/{}", self.prefix(), session_id),
RequestBody::None,
)
.await
}
pub async fn revoke_all_sessions(&self) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let rt = self.client.auth_store().refresh_token();
let body = serde_json::json!({
"refresh_token": rt,
"scope": "global",
});
self.client
.request(
reqwest::Method::POST,
&format!("{}/logout", self.prefix()),
RequestBody::Json(body),
)
.await
}
pub async fn get_user(&self) -> Result<AuraResponse<User>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/user", self.prefix()),
RequestBody::None,
)
.await
}
pub async fn update_user(
&self,
req: UpdateUserRequest,
) -> Result<AuraResponse<User>, AuraError> {
let res = self
.client
.request::<User>(
reqwest::Method::PUT,
&format!("{}/user", self.prefix()),
RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
)
.await?;
if let Some(ref user) = res.data {
let store = self.client.auth_store();
if let (Some(token), Some(rt)) = (store.token(), store.refresh_token()) {
store.save(token, rt, user.clone());
}
}
Ok(res)
}
pub async fn reset_password_for_email(
&self,
req: PasswordResetRequest,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::POST,
&format!("{}/forgot-password", self.prefix()),
RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
)
.await
}
pub async fn update_password(
&self,
new_password: &str,
current_password: Option<&str>,
) -> Result<AuraResponse<User>, AuraError> {
let body = serde_json::json!({
"password": new_password,
"current_password": current_password,
});
self.client
.request(
reqwest::Method::PUT,
&format!("{}/user", self.prefix()),
RequestBody::Json(body),
)
.await
}
pub async fn mfa_enroll(&self) -> Result<AuraResponse<MfaEnrollResponse>, AuraError> {
self.client
.request(
reqwest::Method::POST,
&format!("{}/mfa/setup", self.prefix()),
RequestBody::None,
)
.await
}
pub async fn mfa_verify_setup(
&self,
code: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let body = serde_json::json!({ "code": code });
self.client
.request(
reqwest::Method::POST,
&format!("{}/mfa/setup/verify", self.prefix()),
RequestBody::Json(body),
)
.await
}
pub async fn mfa_verify(
&self,
req: MfaVerifyRequest,
) -> Result<AuraResponse<TokenPair>, AuraError> {
let res = self
.client
.request::<TokenPair>(
reqwest::Method::POST,
&format!("{}/mfa/verify-login", self.prefix()),
RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
)
.await?;
if let Some(ref tp) = res.data {
self.client.auth_store().save(
tp.access_token.clone(),
tp.refresh_token.clone(),
tp.user.clone(),
);
}
Ok(res)
}
pub async fn mfa_unenroll(
&self,
password: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let body = serde_json::json!({ "password": password });
self.client
.request(
reqwest::Method::POST,
&format!("{}/mfa/disable", self.prefix()),
RequestBody::Json(body),
)
.await
}
pub async fn sign_in_with_email_otp(
&self,
email: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let body = serde_json::json!({ "email": email });
self.client
.request(
reqwest::Method::POST,
&format!("{}/email-otp/send", self.prefix()),
RequestBody::Json(body),
)
.await
}
pub async fn verify_otp(
&self,
params: VerifyOtpParams<'_>,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let (chemin, corps) = match params {
VerifyOtpParams::Signup { token } => (
format!("{}/verify-email", self.prefix()),
serde_json::json!({ "token": token }),
),
VerifyOtpParams::MagicLink { token } => (
format!("{}/magic-link/verify", self.prefix()),
serde_json::json!({ "token": token }),
),
VerifyOtpParams::Recovery { token, password } => (
format!("{}/reset-password", self.prefix()),
serde_json::json!({ "token": token, "password": password }),
),
VerifyOtpParams::Email { email, token } => (
format!("{}/email-otp/verify", self.prefix()),
serde_json::json!({ "email": email, "code": token }),
),
VerifyOtpParams::Sms { phone, token } => (
format!("{}/sms/verify", self.prefix()),
serde_json::json!({ "phone": phone, "code": token }),
),
VerifyOtpParams::EmailChange { token } => (
format!("{}/user/confirm-email-change", self.prefix()),
serde_json::json!({ "token": token }),
),
};
let res = self
.client
.request::<serde_json::Value>(reqwest::Method::POST, &chemin, RequestBody::Json(corps))
.await?;
let Some(ref valeur) = res.data else {
return Ok(res);
};
if matches!(params, VerifyOtpParams::EmailChange { .. }) {
if let (Some(acces), Some(rafraichissement), Ok(utilisateur)) = (
self.client.auth_store().token(),
self.client.auth_store().refresh_token(),
serde_json::from_value::<User>(valeur.clone()),
) {
self.client
.auth_store()
.save(acces, rafraichissement, utilisateur);
}
} else if let Ok(paire) = serde_json::from_value::<TokenPair>(valeur.clone()) {
self.client
.auth_store()
.save(paire.access_token, paire.refresh_token, paire.user);
}
Ok(res)
}
pub async fn sign_in_with_sms_otp(
&self,
phone: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let body = serde_json::json!({ "phone": phone });
self.client
.request(
reqwest::Method::POST,
&format!("{}/sms/send", self.prefix()),
RequestBody::Json(body),
)
.await
}
pub async fn list_identities(&self) -> Result<AuraResponse<Vec<IdentityInfo>>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/user/identities", self.prefix()),
RequestBody::None,
)
.await
}
pub async fn link_identity(
&self,
provider: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::POST,
&format!("{}/user/identities/link/{}", self.prefix(), provider),
RequestBody::None,
)
.await
}
pub async fn unlink_identity(
&self,
identity_id: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::DELETE,
&format!("{}/user/identities/{}", self.prefix(), identity_id),
RequestBody::None,
)
.await
}
pub async fn sign_in_with_id_token(
&self,
provider: &str,
id_token: &str,
access_token: Option<&str>,
) -> Result<AuraResponse<TokenPair>, AuraError> {
let body = serde_json::json!({
"provider": provider,
"id_token": id_token,
"access_token": access_token,
});
let res = self
.client
.request::<TokenPair>(
reqwest::Method::POST,
&format!("{}/token", self.prefix()),
RequestBody::Json(body),
)
.await?;
if let Some(ref tp) = res.data {
self.client.auth_store().save(
tp.access_token.clone(),
tp.refresh_token.clone(),
tp.user.clone(),
);
}
Ok(res)
}
pub async fn resend_verification_email(
&self,
email: Option<&str>,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let email_to_send = match email {
Some(e) => Some(e.to_string()),
None => self.client.auth_store().user().and_then(|u| u.email),
};
let body = serde_json::json!({ "email": email_to_send });
self.client
.request(
reqwest::Method::POST,
&format!("{}/resend-verification", self.prefix()),
RequestBody::Json(body),
)
.await
}
pub async fn regenerate_recovery_codes(
&self,
code: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let body = serde_json::json!({ "code": code });
self.client
.request(
reqwest::Method::POST,
&format!("{}/mfa/recovery-codes/regenerate", self.prefix()),
RequestBody::Json(body),
)
.await
}
}
pub struct AuthAdminService {
client: AuraClient,
}
impl AuthAdminService {
pub fn new(client: AuraClient) -> Self {
Self { client }
}
fn prefix(&self) -> String {
"/v1/auth/admin".to_string()
}
pub async fn list_users(
&self,
limit: Option<u32>,
offset: Option<u32>,
search: Option<&str>,
) -> Result<AuraResponse<Vec<AdminUser>>, AuraError> {
let mut query = Vec::new();
if let Some(l) = limit {
query.push(format!("limit={}", l));
}
if let Some(o) = offset {
query.push(format!("offset={}", o));
}
if let Some(s) = search {
query.push(format!("search={}", urlencoding::encode(s)));
}
let qs = if query.is_empty() {
"".to_string()
} else {
format!("?{}", query.join("&"))
};
self.client
.request(
reqwest::Method::GET,
&format!("{}/users{}", self.prefix(), qs),
RequestBody::None,
)
.await
}
pub async fn get_user(&self, user_id: &str) -> Result<AuraResponse<AdminUser>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/users/{}", self.prefix(), user_id),
RequestBody::None,
)
.await
}
pub async fn create_user(
&self,
req: AdminCreateUserRequest,
) -> Result<AuraResponse<AdminUser>, AuraError> {
self.client
.request(
reqwest::Method::POST,
&format!("{}/users", self.prefix()),
RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
)
.await
}
pub async fn update_user(
&self,
user_id: &str,
req: AdminUpdateUserRequest,
) -> Result<AuraResponse<AdminUser>, AuraError> {
self.client
.request(
reqwest::Method::PUT,
&format!("{}/users/{}", self.prefix(), user_id),
RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
)
.await
}
pub async fn delete_user(
&self,
user_id: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::DELETE,
&format!("{}/users/{}", self.prefix(), user_id),
RequestBody::None,
)
.await
}
pub async fn ban_user(
&self,
user_id: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::POST,
&format!("{}/users/{}/ban", self.prefix(), user_id),
RequestBody::None,
)
.await
}
pub async fn unban_user(
&self,
user_id: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::POST,
&format!("{}/users/{}/unban", self.prefix(), user_id),
RequestBody::None,
)
.await
}
pub async fn generate_link(
&self,
user_id: &str,
link_type: &str,
redirect_to: Option<&str>,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
let body = serde_json::json!({
"type": link_type,
"redirect_to": redirect_to,
});
self.client
.request(
reqwest::Method::POST,
&format!("{}/users/{}/generate-link", self.prefix(), user_id),
RequestBody::Json(body),
)
.await
}
pub async fn get_audit_log(
&self,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<AuraResponse<Vec<AuditLogEntry>>, AuraError> {
let mut query = Vec::new();
if let Some(l) = limit {
query.push(format!("limit={}", l));
}
if let Some(o) = offset {
query.push(format!("offset={}", o));
}
let qs = if query.is_empty() {
"".to_string()
} else {
format!("?{}", query.join("&"))
};
self.client
.request(
reqwest::Method::GET,
&format!("{}/audit-log{}", self.prefix(), qs),
RequestBody::None,
)
.await
}
pub async fn get_settings(&self) -> Result<AuraResponse<AuthSettings>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/settings", self.prefix()),
RequestBody::None,
)
.await
}
pub async fn update_settings(
&self,
req: serde_json::Value,
) -> Result<AuraResponse<AuthSettings>, AuraError> {
self.client
.request(
reqwest::Method::PUT,
&format!("{}/settings", self.prefix()),
RequestBody::Json(req),
)
.await
}
pub async fn list_email_templates(
&self,
) -> Result<AuraResponse<Vec<serde_json::Value>>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/email-templates", self.prefix()),
RequestBody::None,
)
.await
}
pub async fn get_email_template(
&self,
template_type: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::GET,
&format!("{}/email-templates/{}", self.prefix(), template_type),
RequestBody::None,
)
.await
}
pub async fn update_email_template(
&self,
template_type: &str,
req: EmailTemplateUpdate,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::PUT,
&format!("{}/email-templates/{}", self.prefix(), template_type),
RequestBody::Json(serde_json::to_value(&req).unwrap_or_default()),
)
.await
}
pub async fn delete_email_template(
&self,
template_type: &str,
) -> Result<AuraResponse<serde_json::Value>, AuraError> {
self.client
.request(
reqwest::Method::DELETE,
&format!("{}/email-templates/{}", self.prefix(), template_type),
RequestBody::None,
)
.await
}
}