diff --git a/.env b/.env index 6511cf4..f61a29f 100644 --- a/.env +++ b/.env @@ -1,6 +1,3 @@ -APP_AUTHOR="Djkáťo" -APP_DESCRIPTION="A simple rust app for doing many things" -APP_NAME="Rust test app" REQUIRED_SALEOR_VERSION="^3.13" SALEOR_APP_ID="dummy-saleor-app-rs" APP_API_BASE_URL="http://10.100.110.234:3000" diff --git a/.env.example b/.env.example index c4f27e6..f61a29f 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,6 @@ -APP_NAME="Rust test app" -REQUIRED_SALEOR_VERSION=">=3.11.7 <4" +REQUIRED_SALEOR_VERSION="^3.13" SALEOR_APP_ID="dummy-saleor-app-rs" -APP_API_BASE_URL="http://localhost:8000/graphql/" -APL="redis" -APL_URL="redis://redis:6379/2" +APP_API_BASE_URL="http://10.100.110.234:3000" +APL="Redis" +APL_URL="redis://localhost:6380/2" LOG_LEVEL="DEBUG" diff --git a/.gitignore b/.gitignore index ea8c4bf..fedaa2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.env diff --git a/Cargo.lock b/Cargo.lock index f7c13f4..b3888bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + [[package]] name = "byteorder" version = "1.5.0" @@ -504,6 +510,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" + [[package]] name = "httparse" version = "1.8.0" @@ -638,6 +650,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -891,7 +913,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -933,7 +955,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] -name = "saleor-app-rs" +name = "saleor-app-sdk" +version = "0.1.0" +dependencies = [ + "anyhow", + "redis", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "saleor-app-template" version = "0.1.0" dependencies = [ "anyhow", @@ -942,9 +976,12 @@ dependencies = [ "dotenvy", "envy", "redis", + "saleor-app-sdk", "serde", "serde_json", "tokio", + "tower", + "tower-http", "tracing", "tracing-serde", "tracing-subscriber", @@ -1213,6 +1250,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.4.2", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -1293,6 +1355,15 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index a719d48..99bc689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,10 @@ -[package] -name = "saleor-app-rs" -version = "0.1.0" -edition = "2021" +[workspace] +members = ["saleor-app-sdk", "saleor-app-template"] +resolver = "2" -[dependencies] +[workspace.dependencies] anyhow = "1.0.79" -#color-eyre = "0.6.2" cynic = "3.4.3" -#dotenvy_macro = "0.15.7" -#poem = {version = "2.0.0", features = ["redis-session"]} serde = "1.0.196" serde_json = "1.0.113" tokio = {version = "1.36.0", features = ["full"]} @@ -19,3 +15,6 @@ tracing-serde = "0.1.3" tracing-subscriber = { version = "0.3.18" } dotenvy = "0.15.7" axum = "0.7.4" +saleor-app-sdk = {path = "saleor-app-sdk"} +tower = { version = "0.4.13", features = ["util"] } +tower-http = { version = "0.5.2", features = ["fs", "trace"] } diff --git a/app logo template.xcf b/app logo template.xcf new file mode 100644 index 0000000..2847cf6 Binary files /dev/null and b/app logo template.xcf differ diff --git a/saleor-app-sdk/Cargo.toml b/saleor-app-sdk/Cargo.toml new file mode 100644 index 0000000..d78e847 --- /dev/null +++ b/saleor-app-sdk/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "saleor-app-sdk" +authors = ["Djkáťo "] +version = "0.1.0" +edition = "2021" +description = "Unofficial Saleor App SDK like library, made to work with rust." +keywords = ["saleor", "sdk", "plugin"] +categories = [ "api-bindings", "web-programming::http-server"] +homepage = "https://github.com/djkato/saleor-app-rs-template" +repository = "https://github.com/djkato/saleor-app-rs-template" +documentation = "https://github.com/djkato/saleor-app-rs-template" +license = "MIT OR Apache-2.0" + +[dependencies] +anyhow.workspace = true +redis = { workspace=true, features = ["aio", "tokio-comp", "connection-manager"] } +serde.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +serde_json.workspace = true diff --git a/saleor-app-sdk/src/apl/env_apl.rs b/saleor-app-sdk/src/apl/env_apl.rs new file mode 100644 index 0000000..caebdb3 --- /dev/null +++ b/saleor-app-sdk/src/apl/env_apl.rs @@ -0,0 +1,29 @@ +use super::APL; +use anyhow::Result; + +#[derive(Clone, Debug)] +/** +is not implemented yet! +*/ +pub struct EnvApl {} + +impl APL for EnvApl { + async fn set(&self, auth_data: crate::AuthData) -> Result<()> { + todo!() + } + async fn get(&self, saleor_api_url: &str) -> Result { + todo!() + } + async fn get_all(&self) -> Result> { + todo!() + } + async fn delete(&self, saleor_api_url: &str) -> Result<()> { + todo!() + } + async fn is_ready(&self) -> Result<()> { + todo!() + } + async fn is_configured(&self) -> Result<()> { + todo!() + } +} diff --git a/saleor-app-sdk/src/apl/file_apl.rs b/saleor-app-sdk/src/apl/file_apl.rs new file mode 100644 index 0000000..8923ed8 --- /dev/null +++ b/saleor-app-sdk/src/apl/file_apl.rs @@ -0,0 +1,34 @@ +use std::path::Path; + +use super::APL; +use anyhow::{bail, Result}; +use std::fs::{read, write}; + +#[derive(Clone, Debug)] +/** +is not implemented yet! +*/ +pub struct FileApl { + pub path: String, +} + +impl APL for FileApl { + async fn set(&self, auth_data: crate::AuthData) -> Result<()> { + todo!() + } + async fn get(&self, saleor_api_url: &str) -> Result { + todo!() + } + async fn get_all(&self) -> Result> { + todo!() + } + async fn delete(&self, saleor_api_url: &str) -> Result<()> { + todo!() + } + async fn is_ready(&self) -> Result<()> { + todo!() + } + async fn is_configured(&self) -> Result<()> { + todo!() + } +} diff --git a/saleor-app-sdk/src/apl/mod.rs b/saleor-app-sdk/src/apl/mod.rs new file mode 100644 index 0000000..4b72085 --- /dev/null +++ b/saleor-app-sdk/src/apl/mod.rs @@ -0,0 +1,24 @@ +pub mod env_apl; +pub mod file_apl; +pub mod redis_apl; + +use crate::AuthData; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::future::Future; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AplType { + Redis, + File, + Env, +} + +pub trait APL: Sized + Send + Sync + Clone + std::fmt::Debug { + fn get(&self, saleor_api_url: &str) -> impl Future> + Send; + fn set(&self, auth_data: AuthData) -> impl Future> + Send; + fn delete(&self, saleor_api_url: &str) -> impl Future> + Send; + fn get_all(&self) -> impl Future>> + Send; + fn is_ready(&self) -> impl Future> + Send; + fn is_configured(&self) -> impl Future> + Send; +} diff --git a/saleor-app-sdk/src/apl/redis_apl.rs b/saleor-app-sdk/src/apl/redis_apl.rs new file mode 100644 index 0000000..6087bec --- /dev/null +++ b/saleor-app-sdk/src/apl/redis_apl.rs @@ -0,0 +1,97 @@ +use std::time::Duration; + +use redis::AsyncCommands; +use tracing::{debug, info}; + +use super::APL; +use crate::AuthData; +use anyhow::{bail, Result}; + +#[derive(Debug, Clone)] +pub struct RedisApl { + pub client: redis::Client, + pub app_api_base_url: String, +} + +impl APL for RedisApl { + async fn get(&self, saleor_api_url: &str) -> Result { + debug!(" get()"); + let mut conn = self.client.get_async_connection().await?; + let val: String = conn.get(self.prepare_key(saleor_api_url)).await?; + debug!("received {val}"); + let val: AuthData = serde_json::from_str(&val)?; + info!("sucessful get"); + debug!("parsed {val}"); + + Ok(val) + } + async fn set(&self, auth_data: AuthData) -> Result<()> { + debug!("set(), {}", auth_data); + let mut conn = self.client.get_async_connection().await?; + conn.set( + self.prepare_key(&auth_data.saleor_api_url), + serde_json::to_string(&auth_data)?, + ) + .await?; + info!("sucessful set"); + Ok(()) + } + async fn delete(&self, saleor_api_url: &str) -> Result<()> { + debug!("delete(), {}", saleor_api_url); + let mut conn = self.client.get_async_connection().await?; + let val: String = conn.get_del(self.prepare_key(saleor_api_url)).await?; + + debug!("sucessful delete(), {}", val); + info!("sucessful del"); + Ok(()) + } + async fn is_ready(&self) -> Result<()> { + debug!("is_ready()"); + let mut conn = self.client.get_async_connection().await?; + let val: String = redis::cmd("INFO") + .arg("server") + .query_async(&mut conn) + .await?; + + debug!("sucessful is_ready(), info: {}", val); + info!("sucessful is_ready"); + Ok(()) + } + async fn is_configured(&self) -> Result<()> { + debug!("is_configured()"); + let mut conn = self.client.get_async_connection().await?; + let val: String = redis::cmd("INFO") + .arg("server") + .query_async(&mut conn) + .await?; + + debug!("sucessful is_configured(), info: {}", val); + info!("sucessful is_configured"); + Ok(()) + } + async fn get_all(&self) -> Result> { + anyhow::bail!("Redis doens't support getall") + } +} + +impl RedisApl { + pub fn new(redis_url: String, app_api_base_url: String) -> Result { + let client = redis::Client::open(redis_url)?; + let mut conn = client.get_connection_with_timeout(Duration::from_secs(3))?; + let val: Result = + redis::cmd("INFO").arg("server").query(&mut conn); + + match val { + Ok(_) => Ok(Self { + client, + app_api_base_url, + }), + Err(e) => bail!("failed redis connection, {:?}", e), + } + } + pub fn prepare_key(&self, saleor_api_url: &str) -> String { + let key = format!("{}:{saleor_api_url}", self.app_api_base_url); + debug!("made key:'{}'", key); + key + } +} diff --git a/saleor-app-sdk/src/headers.rs b/saleor-app-sdk/src/headers.rs new file mode 100644 index 0000000..d95c7a8 --- /dev/null +++ b/saleor-app-sdk/src/headers.rs @@ -0,0 +1,6 @@ +pub const SALEOR_DOMAIN_HEADER: &str = "saleor-domain"; +pub const SALEOR_EVENT_HEADER: &str = "saleor-event"; +pub const SALEOR_SIGNATURE_HEADER: &str = "saleor-signature"; +pub const SALEOR_AUTHORIZATION_BEARER_HEADER: &str = "authorization-bearer"; +pub const SALEOR_API_URL_HEADER: &str = "saleor-api-url"; +pub const SALEOR_SCHEMA_VERSION: &str = "saleor-schema-version"; diff --git a/saleor-app-sdk/src/lib.rs b/saleor-app-sdk/src/lib.rs new file mode 100644 index 0000000..0fa5e1d --- /dev/null +++ b/saleor-app-sdk/src/lib.rs @@ -0,0 +1,40 @@ +pub mod apl; +pub mod headers; +pub mod manifest; +pub mod webhooks; + +use apl::APL; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthToken { + pub auth_token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AuthData { + pub domain: Option, + pub token: String, + pub saleor_api_url: String, + pub app_id: String, + pub jwks: Option, +} +impl std::fmt::Display for AuthData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "(domain:{}\ntoken:{}\nsaleor_api_url:{}\napp_id:{}\njwks:{})", + self.domain.clone().unwrap_or_default(), + self.token, + self.saleor_api_url, + self.app_id, + self.jwks.clone().unwrap_or_default() + ) + } +} + +#[derive(Debug, Clone)] +pub struct SaleorApp { + pub apl: A, +} diff --git a/saleor-app-sdk/src/manifest.rs b/saleor-app-sdk/src/manifest.rs new file mode 100644 index 0000000..952fe6a --- /dev/null +++ b/saleor-app-sdk/src/manifest.rs @@ -0,0 +1,174 @@ +use serde::{Deserialize, Serialize}; + +use crate::webhooks::WebhookManifest; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AppPermission { + ManageUsers, + ManageStaff, + ImpersonateUser, + ManageObservability, + ManageCheckouts, + HandleCheckouts, + HandleTaxes, + ManageTaxes, + ManageChannels, + ManageDiscounts, + ManageGiftCard, + ManageMenus, + ManageOrders, + ManagePages, + ManagePageTypesAndAttributes, + HandlePayments, + ManagePlugins, + ManageProducts, + ManageProductTypesAndAttributes, + ManageShipping, + ManageSettings, + ManageTranslations, + ManageApps, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AppExtensionMount { + ProductDetailsMoreActions, + ProductOverviewCreate, + ProductOverviewMoreActions, + NavigationCatalog, + NavigationOrders, + NavigationCustomers, + NavigationDiscounts, + NavigationTranslations, + NavigationPages, + OrderDetailsMoreActions, + OrderOverviewCreate, + OrderOverviewMoreActions, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum AppExtensionTarget { + Popup, + AppPage, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AppExtension { + /** Name which will be displayed in the dashboard */ + pub label: String, + /** the place where the extension will be mounted */ + pub mount: AppExtensionMount, + /** Method of presenting the interface + `POPUP` will present the interface in a modal overlay + `APP_PAGE` will navigate to the application page + @default `POPUP` + */ + pub target: AppExtensionTarget, + pub permissions: Vec, + /** URL of the view to display, + you can skip the domain and protocol when target is set to `APP_PAGE`, or when your manifest defines an `appUrl`. + + When target is set to `POPUP`, the url will be used to render an `