use std::{env, error::Error}; use poem::{EndpointExt, Route, Server, listener::TcpListener}; use poem_openapi::{ ApiResponse, Object, OpenApi, OpenApiService, SecurityScheme, auth::{Basic, Bearer}, payload::{Json, PlainText}, }; use tokio::task::spawn_blocking; struct Bookmark { #[allow(unused)] id: i32, title: String, url: String, description: String, } #[derive(SecurityScheme)] #[oai(ty = "bearer", key_name = "X-Minibookfluxmark-Auth", key_in = "header")] struct BearerAuth(Bearer); impl BearerAuth { fn check(&self) -> bool { self.0.token == std::env::var("MINIBOOKFLUXMARK_ACCESS_TOKEN").unwrap() } } #[derive(SecurityScheme)] #[oai(ty = "basic")] struct BasicAuth(Basic); impl BasicAuth { fn check(&self) -> bool { self.0.password == std::env::var("MINIBOOKFLUXMARK_ACCESS_TOKEN").unwrap() } } #[derive(SecurityScheme)] enum Auth { BearerAuth(BearerAuth), BasicAuth(BasicAuth), } impl Auth { fn check(&self) -> bool { match self { Self::BasicAuth(a) => a.check(), Self::BearerAuth(a) => a.check(), } } } #[derive(Object, Clone)] struct BookmarkRequest { title: String, url: String, description: String, } #[derive(ApiResponse)] enum AddBookmarkResponse { #[oai(status = 200)] Ok, #[oai(status = 403)] NotAuthorized, #[oai(status = 500)] ServerError, } #[derive(ApiResponse)] enum FeedResponse { #[oai(status = 200)] Ok(PlainText), #[oai(status = 403)] NotAuthorized, #[oai(status = 500)] ServerError, } struct Api; #[OpenApi] impl Api { #[oai(path = "/add", method = "post")] async fn post_bookmark(&self, body: Json, auth: Auth) -> AddBookmarkResponse { if !auth.check() { return AddBookmarkResponse::NotAuthorized; } if let Err(e) = spawn_blocking(move || { let db_conn = match rusqlite::Connection::open( env::var("MINIBOOKFLUXMARK_DB_PATH").unwrap_or(String::from("db.sqlite")), ) { Ok(c) => c, Err(e) => { println!("Failde to open db: {e}"); return Err(e); } }; db_conn.execute( "insert into bookmarks (title, url, description) values (?1, ?2, ?3)", (&body.title, &body.url, &body.description), )?; Ok(()) }) .await { println!("Failed to spawn database save: {e}"); return AddBookmarkResponse::ServerError; } AddBookmarkResponse::Ok } #[oai(path = "/feed", method = "get")] async fn get_feed(&self, auth: Auth) -> FeedResponse { if !auth.check() { return FeedResponse::NotAuthorized; } let mut resp = String::new(); resp.push_str( "\n\n", ); match spawn_blocking(move || { let db_conn = match rusqlite::Connection::open( env::var("MINIBOOKFLUXMARK_DB_PATH").unwrap_or(String::from("db.sqlite")), ) { Ok(c) => c, Err(e) => { println!("Failde to open db: {e}"); return Err(e); } }; let mut query = db_conn.prepare("select id, title, url, description from bookmarks")?; let bookmark_iter = query.query_map([], |row| { Ok(Bookmark { id: row.get(0)?, title: row.get(1)?, url: row.get(2)?, description: row.get(3)?, }) })?; let bookmarks = Vec::from_iter(bookmark_iter.flatten()); Ok(bookmarks) }) .await { Ok(Ok(bookmarks)) => { for b in bookmarks { resp.push_str(&format!( "\n{}\n{}\n{}\n", xml::escape::escape_str_attribute(&b.title), xml::escape::escape_str_attribute(&b.url), xml::escape::escape_str_attribute(&b.description))); } } Ok(Err(e)) => { println!("Failed to read database: {e}"); return FeedResponse::ServerError; } Err(e) => { println!("Failed to spawn database read: {e}"); return FeedResponse::ServerError; } } resp.push_str("\n"); FeedResponse::Ok(PlainText(resp)) } } #[tokio::main] async fn main() -> Result<(), Box> { match spawn_blocking(move || { let db_conn = match rusqlite::Connection::open( env::var("MINIBOOKFLUXMARK_DB_PATH").unwrap_or(String::from("db.sqlite")), ) { Ok(c) => c, Err(e) => { println!("Failde to open db: {e}"); return; } }; db_conn .execute( "create table if not exists bookmarks (id integer primary key, title text not null, url text not null, description text not null)", () ) .unwrap(); }) .await { Ok(_) => (), Err(e) => println!("Failed to spawn database save: {e}"), } let api_service = OpenApiService::new(Api, "minibookfluxmark", "0.1").server("/api/v1"); let ui = api_service.swagger_ui(); let app = Route::new() .nest("/api/v1/", api_service) .nest("/ui/v1/", ui) .with(poem::middleware::Cors::new()); Server::new(TcpListener::bind("0.0.0.0:3000")) .run(app) .await?; Ok(()) }