use std::fmt;
use serde::Deserialize;
use crate::auth::UserAccessConfig;
#[derive(Debug, Clone, Deserialize)]
pub struct ClientAbilitiesHerdClaim {
pub herd_id: i64,
pub slug: String,
#[serde(default)]
pub abilities: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ClientAbilitiesTokenMint {
pub token: String,
#[serde(default)]
pub platform_superadmin: bool,
#[serde(default)]
pub herds: Vec<ClientAbilitiesHerdClaim>,
pub iat: i64,
pub exp: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MintClientAbilitiesTokenError {
MissingUserAccessToken,
MissingSupabaseApiKey,
Unauthorized,
Forbidden,
Http { status: u16, body: String },
InvalidResponse,
}
impl fmt::Display for MintClientAbilitiesTokenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MintClientAbilitiesTokenError::MissingUserAccessToken => {
write!(f, "user access token required to mint client abilities token")
}
MintClientAbilitiesTokenError::MissingSupabaseApiKey => {
write!(f, "supabase API key required to mint client abilities token")
}
MintClientAbilitiesTokenError::Unauthorized => write!(f, "unauthorized"),
MintClientAbilitiesTokenError::Forbidden => write!(f, "forbidden"),
MintClientAbilitiesTokenError::Http { status, body } => {
write!(f, "mint-client-abilities-token failed: HTTP {status} - {body}")
}
MintClientAbilitiesTokenError::InvalidResponse => {
write!(f, "mint-client-abilities-token returned invalid response")
}
}
}
}
pub fn mint_client_abilities_token(
config: &UserAccessConfig,
user_access_token: &str,
) -> Result<ClientAbilitiesTokenMint, MintClientAbilitiesTokenError> {
let token = user_access_token.trim();
if token.is_empty() {
return Err(MintClientAbilitiesTokenError::MissingUserAccessToken);
}
if config.supabase_anon_key.is_empty() {
return Err(MintClientAbilitiesTokenError::MissingSupabaseApiKey);
}
let url = format!("{}/mint-client-abilities-token", functions_url(&config.rest_url));
let response = reqwest::blocking::Client::new()
.post(url)
.header("apikey", &config.supabase_anon_key)
.header("Authorization", format!("Bearer {token}"))
.send()
.map_err(|e| MintClientAbilitiesTokenError::Http {
status: 0,
body: e.to_string(),
})?;
let status = response.status();
let body = response.text().map_err(|e| MintClientAbilitiesTokenError::Http {
status: status.as_u16(),
body: e.to_string(),
})?;
match status.as_u16() {
401 => Err(MintClientAbilitiesTokenError::Unauthorized),
403 => Err(MintClientAbilitiesTokenError::Forbidden),
_ if !status.is_success() => Err(MintClientAbilitiesTokenError::Http {
status: status.as_u16(),
body,
}),
_ => {
let mint: ClientAbilitiesTokenMint =
serde_json::from_str(&body).map_err(|_| MintClientAbilitiesTokenError::InvalidResponse)?;
if mint.token.is_empty() {
Err(MintClientAbilitiesTokenError::InvalidResponse)
} else {
Ok(mint)
}
}
}
}
fn functions_url(rest_url: &str) -> String {
let rest = rest_url.trim_end_matches('/');
let base = rest.strip_suffix("/rest/v1").unwrap_or(rest);
format!("{base}/functions/v1")
}