use std::{convert::Infallible, future::Future, net::SocketAddr};
use axum::{
extract::{ConnectInfo, FromRequestParts, rejection::ExtensionRejection},
http::request::Parts,
};
use fraiseql_core::security::SecurityContext;
use crate::middleware::AuthUser;
pub struct PeerIp(pub String);
impl<S> FromRequestParts<S> for PeerIp
where
S: Send + Sync,
{
type Rejection = Infallible;
fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
let ip = parts
.extensions
.get::<ConnectInfo<SocketAddr>>()
.map_or_else(|| "unknown".to_string(), |ci| ci.0.ip().to_string());
async move { Ok(PeerIp(ip)) }
}
}
#[derive(Debug, Clone)]
pub struct OptionalSecurityContext(pub Option<SecurityContext>);
impl<S> FromRequestParts<S> for OptionalSecurityContext
where
S: Send + Sync + 'static,
{
type Rejection = ExtensionRejection;
#[allow(clippy::manual_async_fn)] fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async move {
let auth_user: Option<AuthUser> = parts.extensions.get::<AuthUser>().cloned();
let headers = &parts.headers;
let security_context = auth_user.map(|auth_user| {
let authenticated_user = auth_user.0;
let request_id = extract_request_id(headers);
let ip_address = extract_ip_address(headers);
let tenant_id = extract_tenant_id(headers);
let mut context = SecurityContext::from_user(&authenticated_user, request_id);
context.ip_address = ip_address;
context.tenant_id = tenant_id.map(fraiseql_core::types::TenantId::new);
for (key, value) in &authenticated_user.extra_claims {
context.attributes.insert(key.clone(), value.clone());
}
if context.tenant_id.is_none() {
if let Some(org_id) =
authenticated_user.extra_claims.get("org_id").and_then(|v| v.as_str())
{
context.tenant_id = Some(fraiseql_core::types::TenantId::new(org_id));
}
}
context
});
Ok(OptionalSecurityContext(security_context))
}
}
}
pub(crate) fn extract_request_id(headers: &axum::http::HeaderMap) -> String {
headers
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map_or_else(|| format!("req-{}", uuid::Uuid::new_v4()), |s| s.to_string())
}
pub(crate) const fn extract_ip_address(_headers: &axum::http::HeaderMap) -> Option<String> {
None
}
pub(crate) const fn extract_tenant_id(_headers: &axum::http::HeaderMap) -> Option<String> {
None
}