use std::fmt;
use anyhow::{anyhow, Result};
use postgrest::Postgrest;
use serde::Deserialize;
use crate::constants::env;
#[derive(Debug, Clone)]
pub struct UserAccessConfig {
pub rest_url: String,
pub supabase_anon_key: String,
}
impl UserAccessConfig {
pub fn from_env() -> Result<Self> {
dotenv::dotenv().ok();
let mut rest_url = std::env::var(env::SCOUT_DATABASE_REST_URL).map_err(|_| {
anyhow!(
"{} environment variable is required",
env::SCOUT_DATABASE_REST_URL
)
})?;
if !rest_url.ends_with("/rest/v1") {
if rest_url.ends_with('/') {
rest_url.push_str("rest/v1");
} else {
rest_url.push_str("/rest/v1");
}
}
let supabase_anon_key = std::env::var(env::SUPABASE_PUBLIC_API_KEY).map_err(|_| {
anyhow!(
"{} environment variable is required",
env::SUPABASE_PUBLIC_API_KEY
)
})?;
Ok(Self {
rest_url,
supabase_anon_key,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HerdRole(pub String);
impl HerdRole {
pub fn system_name(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct UserHerdAccess {
pub herd_role: Option<HerdRole>,
pub is_platform_superadmin: bool,
}
impl UserHerdAccess {
pub fn can_view(&self) -> bool {
self.is_platform_superadmin || self.herd_role.is_some()
}
}
#[derive(Debug, Clone)]
pub struct UserDeviceAccess {
pub herd_id: i64,
pub herd: UserHerdAccess,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthError {
InvalidToken,
NotFound,
ServiceUnavailable,
}
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuthError::InvalidToken => write!(f, "invalid or expired user access token"),
AuthError::NotFound => write!(f, "device not found"),
AuthError::ServiceUnavailable => write!(f, "authorization service unavailable"),
}
}
}
pub async fn get_user_herd_access(
config: &UserAccessConfig,
user_access_token: &str,
herd_id: i64,
) -> Result<UserHerdAccess, AuthError> {
let rows: Vec<AccessRow> = rpc_as_user(
config,
bearer_token(user_access_token)?,
"get_user_herd_access",
serde_json::json!({ "p_herd_id": herd_id }),
)
.await?;
rows.into_iter()
.next()
.map(AccessRow::into_access)
.ok_or(AuthError::ServiceUnavailable)
}
pub async fn get_user_device_access(
config: &UserAccessConfig,
user_access_token: &str,
device_id: i64,
) -> Result<UserDeviceAccess, AuthError> {
let row = rpc_as_user::<AccessRow>(
config,
bearer_token(user_access_token)?,
"get_user_device_access",
serde_json::json!({ "p_device_id": device_id }),
)
.await?
.into_iter()
.next()
.ok_or(AuthError::NotFound)?;
let herd_id = row.herd_id.ok_or(AuthError::ServiceUnavailable)?;
Ok(UserDeviceAccess {
herd_id,
herd: row.into_access(),
})
}
#[derive(Deserialize)]
struct AccessRow {
#[serde(default)]
herd_id: Option<i64>,
herd_role: Option<String>,
is_platform_superadmin: bool,
}
impl AccessRow {
fn into_access(self) -> UserHerdAccess {
UserHerdAccess {
herd_role: self.herd_role.filter(|s| !s.is_empty()).map(HerdRole),
is_platform_superadmin: self.is_platform_superadmin,
}
}
}
fn bearer_token(raw: &str) -> Result<&str, AuthError> {
let s = raw.trim();
let token = if s.len() >= 7 && s[..7].eq_ignore_ascii_case("bearer ") {
s[7..].trim()
} else {
s
};
if token.is_empty() {
Err(AuthError::InvalidToken)
} else {
Ok(token)
}
}
async fn rpc_as_user<T>(
config: &UserAccessConfig,
token: &str,
function_name: &str,
params: serde_json::Value,
) -> Result<Vec<T>, AuthError>
where
T: for<'de> Deserialize<'de>,
{
let response = Postgrest::new(&config.rest_url)
.insert_header("apikey", &config.supabase_anon_key)
.insert_header("Authorization", format!("Bearer {}", token))
.rpc(function_name, params.to_string())
.execute()
.await
.map_err(|_| AuthError::ServiceUnavailable)?;
let status = response.status();
let body = response
.text()
.await
.map_err(|_| AuthError::ServiceUnavailable)?;
if status.as_u16() == 401 {
return Err(AuthError::InvalidToken);
}
if !status.is_success() {
return Err(AuthError::ServiceUnavailable);
}
serde_json::from_str(&body).map_err(|_| AuthError::ServiceUnavailable)
}