use std::{future::Future, sync::Arc};
use topcoat::{
context::Cx,
mail::{Mail, Mailbox},
router::{request::uri, response::Response},
};
use crate::auth::realm::{Realm, current_realm};
pub struct AuthMail {
from: Mailbox,
base_url: String,
}
impl AuthMail {
#[must_use]
pub fn new(from: Mailbox, base_url: impl Into<String>) -> Self {
Self {
from,
base_url: base_url.into().trim_end_matches('/').to_string(),
}
}
pub(crate) fn base_url_is_absolute(&self) -> bool {
self.base_url.parse::<http::Uri>().is_ok_and(|uri| {
matches!(uri.scheme_str(), Some("http" | "https"))
&& uri.authority().is_some()
&& uri.query().is_none()
}) && !self.base_url.contains('#')
}
pub(crate) fn base_url(&self) -> &str {
&self.base_url
}
pub(super) fn link(&self, path: &str, token: &topcoat::session::Token) -> String {
let query = form_urlencoded::Serializer::new(String::new())
.append_pair(TOKEN_FIELD, &token.encode())
.finish();
format!("{}{path}?{query}", self.base_url)
}
pub(super) fn compose(
&self,
address: &str,
subject: String,
text: String,
) -> topcoat::Result<Mail> {
Ok(Mail::builder()
.from(self.from.clone())
.to([Mailbox::new(address)?])
.subject(subject)
.text(text)
.build())
}
}
impl std::fmt::Debug for AuthMail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthMail")
.field("from", &self.from.address())
.field("base_url", &self.base_url)
.finish()
}
}
pub(super) const TOKEN_FIELD: &str = "token";
pub(super) fn token_from_query(cx: &Cx) -> String {
uri(cx)
.query()
.and_then(|query| {
form_urlencoded::parse(query.as_bytes())
.find(|(key, _)| key == TOKEN_FIELD)
.map(|(_, value)| value.into_owned())
})
.unwrap_or_default()
}
pub(super) fn after_response<F, Fut>(cx: &Cx, what: &'static str, send: F)
where
F: FnOnce(Cx, Arc<Realm>) -> Fut + Send + 'static,
Fut: Future<Output = topcoat::Result<()>> + Send + 'static,
{
let Some(panel) = current_realm(cx).map(Arc::clone) else {
return;
};
let cx = cx.clone();
tokio::spawn(async move {
let prefix = panel.prefix.clone();
if let Err(error) = send(cx, panel).await {
tracing::error!(error = %error, panel = %prefix, "{what} not sent");
}
});
}
pub(super) fn private(mut response: Response) -> topcoat::Result<Response> {
response.headers_mut().insert(
http::header::REFERRER_POLICY,
http::HeaderValue::from_static("no-referrer"),
);
Ok(response)
}
pub(super) fn declaration_errors(
auth: &super::AuthConfig,
has_mail: bool,
) -> Vec<crate::DeclarationErrorKind> {
use crate::{AuthMailFault, DeclarationErrorKind};
let mailing = auth.mailing();
if auth.is_disabled() || mailing.is_empty() {
return Vec::new();
}
let Some(mail) = auth.mail.as_ref() else {
return mailing
.into_iter()
.map(|feature| DeclarationErrorKind::AuthMail(AuthMailFault::Unset { feature }))
.collect();
};
let mut faults = Vec::new();
if !mail.base_url_is_absolute() {
faults.push(AuthMailFault::BaseUrl(mail.base_url().to_string()));
}
if !has_mail {
faults.push(AuthMailFault::MissingMail);
}
faults
.into_iter()
.map(DeclarationErrorKind::AuthMail)
.collect()
}