diff --git a/Cargo.toml b/Cargo.toml index de83145..cc79abf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,14 @@ serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" -web-sys = { version = "0.3", features = [ "Request", "Response", "ResponseInit", "Url" ] } +web-sys = { version = "0.3", features = [ + "Headers", + "Request", + "Response", + "ResponseInit", + "Url", + "UrlSearchParams" +] } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/src/lib.rs b/src/lib.rs index ef697e6..1c19174 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,10 @@ #[macro_use] extern crate lazy_static; +#[macro_use] mod utils; mod router; +mod sn; use cfg_if::cfg_if; use utils::{Error, MyResult}; @@ -28,6 +30,7 @@ lazy_static! { fn build_routes() -> router::Router { let mut router = router::Router::new(&default_route); router.add_route("/hello", &hello_world); + sn::build_routes(&mut router); return router; } @@ -45,6 +48,19 @@ async fn hello_world(_req: Request, _url: Url) -> MyResult { #[wasm_bindgen] pub async fn handle_request_rs(req: Request) -> Response { let url = Url::new(&req.url()).unwrap(); + + if req.method() == "OPTIONS" { + return Response::new_with_opt_str_and_init( + None, ResponseInit::new() + .status(200) + .headers({ + let headers = Headers::new().unwrap(); + cors!(headers); + headers + }.as_ref()) + ).unwrap(); + } + let result = ROUTER.execute(req, url).await; match result { diff --git a/src/sn.rs b/src/sn.rs new file mode 100644 index 0000000..9b74b43 --- /dev/null +++ b/src/sn.rs @@ -0,0 +1,147 @@ +// Interface for Standard Notes (Actions) +use crate::router::Router; +use crate::utils::{self, Error, MyResult}; +use serde::{Serialize, Serializer}; +use std::vec::Vec; +use web_sys::*; + +pub fn build_routes(router: &mut Router) { + router.add_route("/actions", &get_actions); +} + +macro_rules! verify_secret { + ($url:expr, $params:ident, $config:ident) => { + let $config = utils::get_config(); + let $params = UrlSearchParams::new_with_str(&$url.search()) + .map_err(|_| Error::BadRequest("Failed to parse query string".into()))?; + if !$params.has("secret") { + return Err(Error::BadRequest("Secret needed".into())); + } else if $params.get("secret").unwrap() != $config.secret { + return Err(Error::Unauthorized("Secret mismatch".into())); + } + }; +} + +async fn get_actions(_req: Request, url: Url) -> MyResult { + verify_secret!(url, params, config); + + let origin = url.origin(); + let mut actions = vec![]; + + actions.push(Action { + label: "Publish".into(), + url: format!("{}/post?secret={}", origin, config.secret.clone()), + verb: Verb::Post, + context: Context::Item, + content_types: vec![ContentType::Note], + access_type: Some(AccessType::Decrypted) + }); + + let info = ActionsExtension { + identifier: config.plugin_identifier.clone(), + name: config.title.clone(), + description: format!("Standard Notes plugin for {}", config.title.clone()), + url: format!("{}/actions?secret={}", origin, config.secret.clone()), + content_type: ContentType::Extension, + supported_types: vec![ContentType::Note], + actions + }; + + Ok(Response::new_with_opt_str_and_init( + Some(&serde_json::to_string(&info) + .map_err(|_| Error::InternalError())?), + ResponseInit::new() + .status(200) + .headers({ + let headers = Headers::new().unwrap(); + headers.set("Content-Type", "application/json").unwrap(); + cors!(headers); + headers + }.as_ref()) + ).unwrap()) +} + +pub enum Verb { + Show, + Post, + Render +} + +impl Serialize for Verb { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + serializer.serialize_str(match *self { + Verb::Show => "show", + Verb::Post => "post", + Verb::Render => "render" + }) + } +} + +pub enum Context { + Item +} + +impl Serialize for Context { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + serializer.serialize_str(match *self { + Context::Item => "Item" + }) + } +} + +pub enum ContentType { + Note, + Extension +} + +impl Serialize for ContentType { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + serializer.serialize_str(match *self { + ContentType::Note => "Note", + ContentType::Extension => "Extension" + }) + } +} + +pub enum AccessType { + Decrypted, + Encrypted +} + +impl Serialize for AccessType { + fn serialize(&self, serializer: S) -> Result + where S: Serializer + { + serializer.serialize_str(match *self { + AccessType::Decrypted => "decrypted", + AccessType::Encrypted => "encrypted" + }) + } +} + +#[derive(Serialize)] +pub struct Action { + label: String, + url: String, + verb: Verb, + context: Context, + content_types: Vec, + access_type: Option +} + +#[derive(Serialize)] +pub struct ActionsExtension { + identifier: String, + name: String, + description: String, + url: String, + content_type: ContentType, + supported_types: Vec, + actions: Vec +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs index a873de3..c4978da 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -17,16 +17,30 @@ cfg_if! { } } +#[macro_export] +macro_rules! cors { + ($headers:ident) => { + $headers.set("Access-Control-Allow-Origin", "*").unwrap(); + $headers.set("Access-Control-Allow-Headers", "*").unwrap(); + }; +} + pub type MyResult = Result; pub enum Error { - NotFound(String) + NotFound(String), + BadRequest(String), + Unauthorized(String), + InternalError() } impl Error { pub fn status_code(&self) -> u16 { match self { - Error::NotFound(_) => 404 + Error::NotFound(_) => 404, + Error::BadRequest(_) => 400, + Error::Unauthorized(_) => 401, + Error::InternalError() => 500 } } } @@ -36,6 +50,15 @@ impl Into for Error { match self { Error::NotFound(reason) => { format!("Not Found, Reason: {}", reason) + }, + Error::BadRequest(reason) => { + format!("Bad Request, Reason: {}", reason) + }, + Error::Unauthorized(reason) => { + format!("Unauthorized, Reason: {}", reason) + }, + Error::InternalError() => { + format!("Internal Errror") } } } @@ -44,7 +67,11 @@ impl Into for Error { #[derive(Deserialize)] pub struct Config { // The secret value used to authenticate the Standard Notes plugin link - secret: String + pub secret: String, + // Title of the blog + pub title: String, + // Plugin identifier used for Standard Notes + pub plugin_identifier: String } pub fn get_config() -> Config {