use std::sync::Arc;
use leptoskit_db::Context;
use leptoskit_query::BoxFuture;
use crate::Error;
#[derive(Clone, Debug, Default)]
pub struct IdentityLease {
pub continuity: Option<String>,
pub reads: Vec<leptoskit_db::ReadEntry>,
pub expires_at: Option<std::time::SystemTime>,
pub refresh_at: Option<std::time::SystemTime>,
}
#[derive(Clone, Default)]
pub struct Identity {
pub context: Context,
pub caller: Option<String>,
}
impl Identity {
pub fn anonymous() -> Self {
Self::default()
}
pub fn new(context: Context) -> Self {
Self {
context,
caller: None,
}
}
#[must_use]
pub fn with_caller(mut self, caller: impl Into<String>) -> Self {
self.caller = Some(caller.into());
self
}
pub fn of(db: &impl HasContext) -> Self {
Self::new(db.context_or_default())
}
}
impl std::fmt::Debug for Identity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Identity")
.field("caller", &self.caller)
.finish_non_exhaustive()
}
}
#[doc(hidden)]
pub trait HasContext {
fn context_or_default(&self) -> Context;
}
#[cfg(any(feature = "postgres", feature = "sqlite"))]
impl HasContext for leptoskit_db::Db {
fn context_or_default(&self) -> Context {
self.context().cloned().unwrap_or_default()
}
}
pub trait Identify: Send + Sync + 'static {
fn identify<'a>(
&'a self,
request: &'a http::request::Parts,
) -> BoxFuture<'a, Result<Identity, Error>>;
fn lease<'a>(&'a self, _identity: &'a Identity) -> BoxFuture<'a, Result<IdentityLease, Error>> {
Box::pin(std::future::ready(Ok(IdentityLease::default())))
}
fn identify_token<'a>(&'a self, token: &'a str) -> BoxFuture<'a, Result<Identity, Error>> {
Box::pin(async move {
let mut request = http::Request::new(());
let value = http::HeaderValue::from_str(&format!("Bearer {token}"))
.map_err(|_| Error::unauthorized("the token isn't valid in a header"))?;
request
.headers_mut()
.insert(http::header::AUTHORIZATION, value);
let (parts, ()) = request.into_parts();
self.identify(&parts).await
})
}
}
impl<I: Identify> Identify for Arc<I> {
fn identify<'a>(
&'a self,
request: &'a http::request::Parts,
) -> BoxFuture<'a, Result<Identity, Error>> {
(**self).identify(request)
}
fn lease<'a>(&'a self, identity: &'a Identity) -> BoxFuture<'a, Result<IdentityLease, Error>> {
(**self).lease(identity)
}
fn identify_token<'a>(&'a self, token: &'a str) -> BoxFuture<'a, Result<Identity, Error>> {
(**self).identify_token(token)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Anonymous;
impl Identify for Anonymous {
fn identify<'a>(
&'a self,
_request: &'a http::request::Parts,
) -> BoxFuture<'a, Result<Identity, Error>> {
Box::pin(std::future::ready(Ok(Identity::anonymous())))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Header;
impl Identify for Header {
fn identify<'a>(
&'a self,
request: &'a http::request::Parts,
) -> BoxFuture<'a, Result<Identity, Error>> {
Box::pin(async move {
let header = request
.headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| Error::unauthorized("no token"))?;
Ok(Identity::new(Context::new().with(header.to_owned())).with_caller(header))
})
}
}
#[tokio::test]
async fn a_token_identifies_as_a_bearer_request_would() {
let identity = Arc::new(Header)
.identify_token("abc")
.await
.expect("a token");
assert_eq!(identity.caller.as_deref(), Some("Bearer abc"));
assert_eq!(
identity.context.get::<String>().map(String::as_str),
Some("Bearer abc")
);
assert!(format!("{identity:?}").contains("Bearer abc"));
assert!(matches!(
Header.identify_token("bad\ntoken").await,
Err(Error::Unauthorized(_))
));
let (parts, ()) = http::Request::new(()).into_parts();
let nobody = Anonymous.identify(&parts).await.expect("anonymous");
assert!(nobody.caller.is_none());
assert!(Identity::anonymous().context.get::<String>().is_none());
}
}