/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin */ use crate::{ State, auth::token_to_user, routes::error::{MyError, MyResult}, }; use anyhow::anyhow; use jellycommon::{USER_ADMIN, User, jellyobject::Object}; use jellyui::{Page, RenderInfo, Scaffold}; use rocket::{ Request, async_trait, http::{MediaType, Status}, request::{FlashMessage, FromRequest, Outcome}, response::content::RawHtml, }; use std::sync::Arc; pub struct RequestInfo<'a> { pub lang: &'a str, pub accept: Accept, pub debug: &'a str, pub user: Option>, pub state: Arc, pub no_scaffold: bool, pub flash: Option>, } #[async_trait] impl<'r> FromRequest<'r> for RequestInfo<'r> { type Error = MyError; async fn from_request(request: &'r Request<'_>) -> Outcome { match Self::from_request_ut(request).await { Ok(a) => Outcome::Success(a), Err(a) => Outcome::Error((Status::BadRequest, a)), } } } impl<'a> RequestInfo<'a> { pub async fn from_request_ut(request: &'a Request<'_>) -> MyResult { let state: &Arc = request.rocket().state().unwrap(); Ok(Self { lang: accept_language(request), accept: Accept::from_request_ut(request), user: user_from_request(state, request)?, state: state.clone(), no_scaffold: request.query_value::("no_scaff").is_some(), debug: request .query_value::<&str>("debug") .transpose() .unwrap() .unwrap_or("none"), flash: FlashMessage::from_request(request).await.succeeded(), }) } pub fn require_user(&'a self) -> MyResult<&'a Object> { self.user .as_deref() .ok_or(MyError(anyhow!("user required"))) } pub fn require_admin(&'a self) -> MyResult<&'a Object> { let user = self.require_user()?; if !user.has(USER_ADMIN.0) { Err(anyhow!("admin required"))? } Ok(user) } pub fn render_info(&'a self) -> RenderInfo<'a> { RenderInfo { lang: self.lang, status_message: None, user: self.user.as_deref(), config: &self.state.config.ui, message: self.flash.as_ref().map(|f| (f.kind(), f.message())), } } pub fn respond_ui(&self, page: &dyn Page) -> RawHtml { if self.no_scaffold { RawHtml(page.render().to_string()) } else { RawHtml(Scaffold { page }.to_string()) } } } #[derive(Debug, Default)] pub enum Accept { #[default] Other, Json, Image, Media, Html, } impl Accept { pub fn from_request_ut(request: &Request) -> Self { if let Some(a) = request.accept() { if a.preferred().exact_eq(&MediaType::JSON) { Accept::Json } else { Accept::Other } } else { Accept::Other } } pub fn is_json(&self) -> bool { matches!(self, Self::Json) } } fn accept_language<'a>(request: &'a Request<'_>) -> &'a str { request .headers() .get_one("accept-language") .and_then(|h| { h.split(",") .filter_map(|e| { let code = e.split(";").next()?; let code = code.split_once("-").unwrap_or((code, "")).0; Some(code) }) .next() }) .unwrap_or("en") } fn user_from_request(state: &State, req: &Request<'_>) -> Result>, MyError> { let Some(token) = req .query_value("session") .map(|e| e.unwrap()) .or_else(|| req.query_value("api_key").map(|e| e.unwrap())) .or_else(|| req.headers().get_one("X-MediaBrowser-Token")) .or_else(|| { req.headers() .get_one("Authorization") .and_then(parse_jellyfin_auth) }) // for jellyfin compat .or(req.cookies().get("session").map(|cookie| cookie.value())) else { return Ok(None); }; // jellyfin urlescapes the token for *some* requests let token = token.replace("%3D", "="); Ok(Some(token_to_user(state, &token)?)) } fn parse_jellyfin_auth(h: &str) -> Option<&str> { for tok in h.split(" ") { if let Some(tok) = tok.strip_prefix("Token=\"") && let Some(tok) = tok.strip_suffix("\"") { return Some(tok); } } None }