use crate::{ error::{Error, OptionExt}, State, }; use actix_session::{Session, UserSession}; use actix_web::{ dev::{Payload, Service, ServiceRequest, ServiceResponse, Transform}, http::StatusCode, web::{Data, Path}, FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError, }; use event_listener::Event; use futures::future::LocalBoxFuture; use hyaenidae_accounts::Authenticated; use hyaenidae_profiles::store::{Profile, Submission}; use std::{ cell::{Cell, RefCell}, future::{ready, Ready}, rc::Rc, }; use uuid::Uuid; pub(crate) struct Referer(pub(crate) String); impl FromRequest for Referer { type Config = (); type Error = actix_web::Error; type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let optionable = || { let value = req.headers().get("Referer")?; let s = value.to_str().ok()?; Some(Referer(s.to_owned())) }; ready((optionable)().ok_or_else(|| Error::Required.into())) } } pub struct CurrentSubmission(pub Submission); #[derive(Clone, Debug, serde::Deserialize)] struct SubmissionPath { submission_id: Uuid, } impl FromRequest for CurrentSubmission { type Config = (); type Error = actix_web::Error; type Future = LocalBoxFuture<'static, Result>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let state_fut = Data::::extract(&req); let submission_id_fut = Path::::extract(&req); Box::pin(async move { let submission_id = submission_id_fut.await?.submission_id; let state = state_fut.await?; let store = state.profiles.clone(); let submission = actix_web::web::block(move || store.store.submissions.by_id(submission_id)?.req()) .await??; Ok(CurrentSubmission(submission)) }) } } pub(crate) struct UserProfile(pub Profile); impl UserProfile { pub fn into_inner(self) -> Profile { self.0 } } impl FromRequest for UserProfile { type Config = (); type Error = actix_web::Error; type Future = LocalBoxFuture<'static, Result>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let opt = req .extensions() .get::() .map(|extractor| (extractor.clone(), extractor.2.listen())); let user_fut = Authenticated::extract(&req); let state_fut = Data::::extract(&req); Box::pin(async move { let (extractor, listen_fut) = match opt { Some(tuple) => tuple, None => return Err(ServerError("/404".to_owned()).into()), }; if let Some(profile) = extractor.0.borrow().as_ref() { return Ok(UserProfile(profile.clone())); } if !extractor.1.get() { listen_fut.await; if let Some(profile) = extractor.0.borrow().as_ref() { return Ok(UserProfile(profile.clone())); } } let state = state_fut.await?; let user_id = user_fut.await?.user().id(); let has_profiles = state .profiles .store .profiles .for_local(user_id) .next() .is_some(); let redirect = if has_profiles { "/profiles/change" } else { "/profiles/create/handle" }; Err(ServerError(redirect.to_owned()).into()) }) } } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub(super) struct ProfileData { id: Uuid, } impl ProfileData { pub(super) fn set_data(id: Uuid, session: &Session) -> Result<(), SessionError> { session .insert("profile-data", ProfileData { id }) .map_err(|_| SessionError::Set) } fn data(session: &Session) -> Result, actix_web::Error> { Ok(session.get("profile-data")?) } } #[derive(Debug, thiserror::Error)] pub(super) enum SessionError { #[error("Error setting key")] Set, } #[derive(Debug, thiserror::Error)] #[error("Redirecting to {0}")] struct ServerError(String); impl ResponseError for ServerError { fn status_code(&self) -> StatusCode { StatusCode::SEE_OTHER } fn error_response(&self) -> HttpResponse { HttpResponse::SeeOther() .insert_header(("Location", self.0.clone())) .finish() } } pub(crate) struct CurrentProfile(pub State); pub(crate) struct ProfileMiddleware(State, S); #[derive(Clone)] struct ProfileExtractor(Rc>>, Rc>, Rc); struct ProfileDropGuard(Rc, Rc>); fn profile_extractor() -> (ProfileExtractor, ProfileDropGuard) { let event = Rc::new(Event::new()); let state = Rc::new(RefCell::new(None)); let flag = Rc::new(Cell::new(false)); ( ProfileExtractor(state, Rc::clone(&flag), Rc::clone(&event)), ProfileDropGuard(event, flag), ) } impl Drop for ProfileDropGuard { fn drop(&mut self) { self.1.set(true); self.0.notify(usize::MAX); } } impl Transform for CurrentProfile where S: Service, Error = actix_web::Error>, S::Future: 'static, { type Response = S::Response; type Error = S::Error; type InitError = (); type Transform = ProfileMiddleware; type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ready(Ok(ProfileMiddleware(self.0.clone(), service))) } } impl Service for ProfileMiddleware where S: Service, Error = actix_web::Error>, S::Future: 'static, { type Response = S::Response; type Error = S::Error; type Future = LocalBoxFuture<'static, Result>; fn poll_ready( &self, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { self.1.poll_ready(cx) } fn call(&self, req: ServiceRequest) -> Self::Future { let session = req.get_session(); let (extractor, drop_guard) = profile_extractor(); req.extensions_mut().insert(extractor.clone()); let (req, pl) = req.into_parts(); let user_fut = Authenticated::extract(&req); let req = ServiceRequest::from_parts(req, pl); let fut = self.1.call(req); let state = self.0.clone(); Box::pin(async move { let user_id = if let Ok(auth) = user_fut.await { auth.user().id() } else { drop(drop_guard); return fut.await; }; if let Some(ProfileData { id: profile_id }) = ProfileData::data(&session)? { match actix_web::web::block(move || { Ok(state.profiles.store.profiles.by_id(profile_id)?) as Result, Error> }) .await { Ok(Ok(Some(profile))) => { if profile .local_owner() .map(|id| id == user_id) .unwrap_or(false) { *extractor.0.borrow_mut() = Some(profile); } } Ok(Ok(None)) => { log::warn!("No profile found for {}", profile_id); } Ok(Err(e)) => { log::warn!("Error fetching profile {}: {}", profile_id, e); } Err(_) => { log::warn!("Panic while fetching profile {}", profile_id); } } } drop(drop_guard); fut.await }) } }