use actix_web::web::Data; use actix_web::{get, Scope}; use actix_web_httpauth::middleware::HttpAuthentication; use serde::{Deserialize, Serialize}; use crate::{api, auth, AppState}; #[macro_export] #[cfg(test)] macro_rules! call_endpoint { ($req:ident, $state:ident) => {{ // let subscriber = tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt::with( // tracing_subscriber::registry(), // tracing_subscriber::Layer::with_filter( // tracing_subscriber::fmt::Layer::new() // .pretty() // .with_writer(std::io::stdout) // .with_ansi(true), // tracing_subscriber::filter::LevelFilter::DEBUG, // ), // ); // tracing::subscriber::set_global_default(subscriber) // .expect("Unable to set a global collector"); let jwt_secret = crate::auth::get_secret(&$state.db).await?; let token = jsonwebtoken::encode( &jsonwebtoken::Header::default(), &crate::auth::AuthClaims { exp: 10_000_000_000, }, &jsonwebtoken::EncodingKey::from_secret(&jwt_secret), ) .map_err(|e| { crate::error::Error::new( crate::error::ErrorCode::Internal, &format!("Token error: {} ", e), ) })?; $req.headers_mut().append( actix_web::http::header::AUTHORIZATION, actix_web::http::header::HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(), ); let a = App::new() .wrap(tracing_actix_web::TracingLogger::default()) .app_data($state.clone()) .service(routes()); let app = actix_web::test::init_service(a).await; let resp = actix_web::test::call_service(&app, $req).await; resp }}; } #[cfg(test)] macro_rules! get_response { ($resp: ident, $type:ty) => {{ let body = test::read_body($resp).await.to_vec(); serde_json::from_slice::<$type>(&body).unwrap() }}; } pub mod application_categories; pub mod applications; pub mod authorization; pub mod bookmark_categories; pub mod bookmarks; pub mod settings; mod api_prelude { pub use super::ListObjects; pub use crate::entity::prelude::*; pub use crate::entity::*; pub use crate::AppState; pub use actix_web::{delete, get, post, put, web, Error, HttpResponse, Scope}; pub use sea_orm::prelude::*; pub use sea_orm::{NotSet, Set}; pub use serde::{Deserialize, Serialize}; } #[cfg(test)] pub mod test_prelude { use std::collections::HashMap; pub use super::ListObjects; use crate::auth; pub use crate::entity::*; pub use crate::AppState; pub use super::routes; pub use crate::error::Result; pub use actix_web::dev::ServiceResponse; pub use actix_web::{test, web, App}; pub use sea_orm::{ entity::prelude::*, entity::*, tests_cfg::*, DatabaseBackend, MockDatabase, MockExecResult, Transaction, }; use tokio::sync::Mutex; /// Sets up a testing state with an in-memory database and creates the scheme. pub async fn setup_state() -> Result> { let pool = sqlx::SqlitePool::connect("sqlite::memory:").await?; sqlx::migrate!("./migrations").run(&pool).await?; let db = sea_orm::SqlxSqliteConnector::from_sqlx_sqlite_pool(pool); auth::generate_secret(&db).await?; Ok(actix_web::web::Data::new(AppState { db, healthcheck_status: Mutex::new(HashMap::new()), data_path: ".".to_string(), })) } } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListObjects where T: Serialize, { items: Vec, total: usize, } impl ListObjects { pub fn new(items: Vec, total: usize) -> Self { Self { items, total } } } use actix_multipart::Multipart; use actix_web::{post, web, HttpResponse}; use futures::StreamExt; use std::io::Write; use std::path::Path; #[post("/upload/bg")] async fn bg_upload( mut payload: Multipart, state: Data, ) -> crate::error::Result { if let Some(Ok(mut field)) = payload.next().await { let file_path = Path::new(&state.data_path).join("bg.jpg"); let mut f = web::block(|| std::fs::File::create(file_path)).await??; while let Some(chunk) = field.next().await { let data = chunk.unwrap(); f = web::block(move || f.write_all(&data).map(|_| f)).await??; } } Ok(HttpResponse::Ok().body("")) } #[derive(Deserialize)] struct IconRequest { url: String, } #[get("/icon_proxy")] async fn icon_proxy(q: web::Query) -> crate::error::Result { let url = url::Url::parse(&q.url).map_err(|_| "Could not parse url")?; let client = reqwest::Client::builder() .danger_accept_invalid_certs(true) .referer(false) .build() .unwrap(); let resp = client .request(reqwest::Method::GET, q.url.clone()) .timeout(std::time::Duration::from_secs(10)) .send() .await?; let html = resp.text().await?; let document = scraper::Html::parse_document(&html); let icon_selector = scraper::Selector::parse(r#"link[rel="icon"]"#).map_err(|_| "Error getting selector")?; let shortcut_selector = scraper::Selector::parse(r#"link[rel="shortcut icon"]"#) .map_err(|_| "Error getting selector")?; let icon_url = if let Some(src) = document .select(&icon_selector) .chain(document.select(&shortcut_selector)) .next() .and_then(|link| link.value().attr("href")) { url.join(src).map_err(|_| "could not map base url")? } else { let url = url::Url::parse(&q.url).map_err(|_| "Could not parse url")?; let url_string = &format!( "{}://{}/favicon.ico", url.scheme(), url.host_str().unwrap_or_default() ); tracing::info!("Url string: {}", url_string); url::Url::parse(url_string).map_err(|_| "Couldnt parse url")? }; let img_bytes = client .request(reqwest::Method::GET, icon_url.clone()) .timeout(std::time::Duration::from_secs(5)) .send() .await? .bytes() .await?; let fpath = Path::new( icon_url .path_segments() .and_then(|i| i.rev().next()) .unwrap_or("test.png"), ); let ext = fpath .extension() .map(|s| s.to_str().unwrap()) .unwrap_or(".png"); Ok(HttpResponse::Ok() .content_type(mime_guess::from_ext(ext).first_or_octet_stream().as_ref()) .body(img_bytes)) } pub fn routes() -> Scope { let auth_handler = HttpAuthentication::bearer(auth::validator); let protected_routes = Scope::new("") .wrap(auth_handler) .service(api::applications::routes()) .service(api::application_categories::routes()) .service(api::bookmarks::routes()) .service(api::bookmark_categories::routes()) .service(api::settings::routes()) .service(api::authorization::update_password) .service(bg_upload); Scope::new("api") .service(api::authorization::authorize) .service(api::authorization::initial_setup) .service(icon_proxy) .service(protected_routes) }