243 lines
7.4 KiB
Rust
243 lines
7.4 KiB
Rust
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<actix_web::web::Data<AppState>> {
|
|
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<T>
|
|
where
|
|
T: Serialize,
|
|
{
|
|
items: Vec<T>,
|
|
total: usize,
|
|
}
|
|
|
|
impl<T: Serialize> ListObjects<T> {
|
|
pub fn new(items: Vec<T>, 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<AppState>,
|
|
) -> crate::error::Result<HttpResponse> {
|
|
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<IconRequest>) -> crate::error::Result<HttpResponse> {
|
|
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)
|
|
}
|