diff --git a/.github/rust.yml b/.github/rust.yml deleted file mode 100644 index b76e204..0000000 --- a/.github/rust.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Rust - -on: - push: - branches: [master] - pull_request: - branches: [master] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Cache cargo registry - uses: actions/cache@v1 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - name: Cache cargo index - uses: actions/cache@v1 - with: - path: ~/.cargo/git - key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - - name: Cache cargo build - uses: actions/cache@v1 - with: - path: target - key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - - name: Run Linter - run: cargo fmt -- --check && cargo clippy --all-targets --all-features -- -D warnings - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose diff --git a/Cargo.lock b/Cargo.lock index 193d42e..b8de512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -535,6 +535,26 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "cookie" version = "0.14.4" @@ -2674,6 +2694,26 @@ dependencies = [ [[package]] name = "simple-payment-gateway" version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "const_format", + "cynic", + "cynic-codegen", + "dotenvy", + "envy", + "redis", + "saleor-app-sdk", + "serde", + "serde_json", + "surf", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-serde", + "tracing-subscriber", +] [[package]] name = "simple_asn1" @@ -3406,6 +3446,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "universal-hash" version = "0.4.0" diff --git a/README.md b/README.md index 947f972..f63d28d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ to use in your project inside this repo, create a new workspace member and add ` ## Creating a new Saleor App from template -If using the `saleor-app-template`, create a new workspace member `cargo new `, then `cp saleor-app-template/* `. +If using the `saleor-app-template`, create a new workspace member `cargo new `,`rm -rf /*` then `cp -r app-template/* /`. ## Adding new dependencies diff --git a/app-template/schema/note.txt b/app-template/schema/note.txt deleted file mode 100644 index 60404ad..0000000 --- a/app-template/schema/note.txt +++ /dev/null @@ -1 +0,0 @@ -To update the schema, you can download it from https://raw.githubusercontent.com/saleor/saleor/main/saleor/graphql/schema.graphql diff --git a/sdk/src/headers.rs b/sdk/src/headers.rs index 4079d3f..3c10425 100644 --- a/sdk/src/headers.rs +++ b/sdk/src/headers.rs @@ -25,27 +25,3 @@ pub struct SaleorHeaders<'a> { #[serde(rename = "content-length")] content_length: u16, } - -/* TODO! -impl SaleorHeaders { - pub fn verify(&self, payload: &str) -> anyhow::Result<()> { - /* - if let Some(saleor_signature) = self.signature { - let split: Vec = saleor_signature.split(".").collect(); - let header = split.get(0); - let signature = split.get(2); - if let Some(header) = header { - if let Some(signature) = signature { - let jws = jose_jws::Signature { - signature: signature.into(), - header, - protected: None, - }; - } - } - } - */ - todo!() - } -} -*/ diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs index 80a97b2..4679875 100644 --- a/sdk/src/manifest.rs +++ b/sdk/src/manifest.rs @@ -18,6 +18,7 @@ pub enum AppPermission { ManageGiftCard, ManageMenus, ManageOrders, + ManageOrdersImport, ManagePages, ManagePageTypesAndAttributes, HandlePayments, diff --git a/simple-payment-gateway/Cargo.toml b/simple-payment-gateway/Cargo.toml index a06934e..a980817 100644 --- a/simple-payment-gateway/Cargo.toml +++ b/simple-payment-gateway/Cargo.toml @@ -2,5 +2,38 @@ name = "simple-payment-gateway" version = "0.1.0" edition = "2021" +authors = ["Djkáťo "] +description = "Payment gateway that adds payment methods that don't need actual verification: Cash on delivery, Cash on warehouse pickup, bank tranfer." +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" +keywords = ["saleor", "sdk", "plugin", "template"] +categories = ["api-bindings", "web-programming::http-server"] +license = "PolyForm-Noncommercial-1.0.0" [dependencies] +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio = { workspace = true, features = ["full"] } +redis = { workspace = true, features = [ + "aio", + "tokio-comp", + "connection-manager", +] } +envy.workspace = true +tracing.workspace = true +tracing-serde.workspace = true +tracing-subscriber.workspace = true +dotenvy.workspace = true +axum.workspace = true +saleor-app-sdk.workspace = true +tower = { workspace = true, features = ["util"] } +tower-http = { workspace = true, features = ["fs", "trace"] } +surf.workspace = true +cynic = { workspace = true, features = ["http-surf"] } +cynic-codegen.workspace = true +const_format = "0.2.32" + +[build-dependencies] +cynic-codegen.workspace = true diff --git a/simple-payment-gateway/Dockerfile b/simple-payment-gateway/Dockerfile new file mode 100644 index 0000000..422a68f --- /dev/null +++ b/simple-payment-gateway/Dockerfile @@ -0,0 +1,19 @@ +FROM lukemathwalker/cargo-chef:latest as chef +WORKDIR /app + +FROM chef AS planner +COPY ./Cargo.toml ./Cargo.lock ./ +COPY ./src ./src +RUN cargo chef prepare + +FROM chef AS builder +COPY --from=planner /app/recipe.json . +RUN cargo chef cook --release +COPY . . +RUN cargo build --release +RUN mv ./target/release/saleor-app-template ./app + +FROM debian:stable-slim AS runtime +WORKDIR /app +COPY --from=builder /app/app /usr/local/bin/ +ENTRYPOINT ["/usr/local/bin/app"] diff --git a/simple-payment-gateway/README.md b/simple-payment-gateway/README.md new file mode 100644 index 0000000..02a223d --- /dev/null +++ b/simple-payment-gateway/README.md @@ -0,0 +1,4 @@ +# Unofficial Saleor App Template + +To update the saleor schema, you can download it from [here](https://raw.githubusercontent.com/saleor/saleor/main/saleor/graphql/schema.graphql) and put into schema/schema.graphql +To generate typings for events and gql queries, use: https://generator.cynic-rs.dev/ diff --git a/simple-payment-gateway/build.rs b/simple-payment-gateway/build.rs new file mode 100644 index 0000000..6b8ffae --- /dev/null +++ b/simple-payment-gateway/build.rs @@ -0,0 +1,7 @@ +fn main() { + cynic_codegen::register_schema("saleor") + .from_sdl_file("schema/schema.graphql") + .unwrap() + .as_default() + .unwrap(); +} diff --git a/simple-payment-gateway/public/logo.png b/simple-payment-gateway/public/logo.png new file mode 100644 index 0000000..f591130 Binary files /dev/null and b/simple-payment-gateway/public/logo.png differ diff --git a/sitemap-generator/schema/schema.graphql b/simple-payment-gateway/schema/schema.graphql similarity index 100% rename from sitemap-generator/schema/schema.graphql rename to simple-payment-gateway/schema/schema.graphql diff --git a/simple-payment-gateway/src/app.rs b/simple-payment-gateway/src/app.rs new file mode 100644 index 0000000..14944ec --- /dev/null +++ b/simple-payment-gateway/src/app.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; + +use saleor_app_sdk::{config::Config, manifest::AppManifest, SaleorApp}; +// Make our own error that wraps `anyhow::Error`. +pub struct AppError(anyhow::Error); + +// Tell axum how to convert `AppError` into a response. +impl IntoResponse for AppError { + fn into_response(self) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0), + ) + .into_response() + } +} + +// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into +// `Result<_, AppError>`. That way you don't need to do that manually. +impl From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} + +pub fn trace_to_std(config: &Config) { + tracing_subscriber::fmt() + .with_max_level(config.log_level) + .with_target(false) + .init(); +} + +#[derive(Debug, Clone)] +pub struct AppState { + pub saleor_app: Arc>, + pub config: Config, + pub manifest: AppManifest, +} diff --git a/simple-payment-gateway/src/main.rs b/simple-payment-gateway/src/main.rs index e7a11a9..93a875b 100644 --- a/simple-payment-gateway/src/main.rs +++ b/simple-payment-gateway/src/main.rs @@ -1,3 +1,94 @@ -fn main() { - println!("Hello, world!"); +#![allow(non_upper_case_globals)] +#![feature(let_chains)] +#![deny(clippy::unwrap_used, clippy::expect_used)] +mod app; +mod queries; +mod routes; + +use anyhow::Context; +use saleor_app_sdk::{ + config::Config, + manifest::{AppManifest, AppPermission}, + webhooks::{SyncWebhookEventType, WebhookManifest}, + SaleorApp, +}; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::{ + app::{trace_to_std, AppState}, + queries::event_transactions::{ + sub_payment_gateway_initialize_session, sub_transaction_charge_requested, + sub_transaction_initialize_session, sub_transaction_process_session, + sub_transaction_refund_requested, + }, + routes::create_routes, +}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let config = Config::load()?; + trace_to_std(&config); + + let saleor_app = SaleorApp::new(&config)?; + + let app_manifest = AppManifest::new(&config) + .add_webhook( + WebhookManifest::new(&config) + .set_query(sub_transaction_process_session) + .add_sync_event(SyncWebhookEventType::TransactionProcessSession) + .build(), + ) + .add_webhook( + WebhookManifest::new(&config) + .set_query(sub_transaction_charge_requested) + .add_sync_event(SyncWebhookEventType::TransactionChargeRequested) + .build(), + ) + .add_webhook( + WebhookManifest::new(&config) + .set_query(sub_transaction_refund_requested) + .add_sync_event(SyncWebhookEventType::TransactionRefundRequested) + .build(), + ) + .add_webhook( + WebhookManifest::new(&config) + .set_query(sub_transaction_initialize_session) + .add_sync_event(SyncWebhookEventType::TransactionInitializeSession) + .build(), + ) + .add_webhook( + WebhookManifest::new(&config) + .set_query(sub_payment_gateway_initialize_session) + .add_sync_event(SyncWebhookEventType::PaymentGatewayInitializeSession) + .build(), + ) + .add_permissions(vec![ + AppPermission::HandlePayments, + AppPermission::ManageOrders, + AppPermission::ManageCheckouts, + AppPermission::HandleCheckouts, + ]) + .build(); + let app_state = AppState { + manifest: app_manifest, + config: config.clone(), + saleor_app: Arc::new(Mutex::new(saleor_app)), + }; + let app = create_routes(app_state); + + let listener = tokio::net::TcpListener::bind( + &config + .app_api_base_url + .split("//") + .collect::>() + .get(1) + .context("APP_API_BASE_URL invalid format")?, + ) + .await?; + tracing::debug!("listening on {}", listener.local_addr().unwrap()); + match axum::serve(listener, app).await { + Ok(o) => Ok(o), + Err(e) => anyhow::bail!(e), + } } diff --git a/simple-payment-gateway/src/queries/event_transactions.rs b/simple-payment-gateway/src/queries/event_transactions.rs new file mode 100644 index 0000000..6695f3b --- /dev/null +++ b/simple-payment-gateway/src/queries/event_transactions.rs @@ -0,0 +1,180 @@ +use const_format::concatcp; +#[cynic::schema("saleor")] +mod schema {} + +pub const fragment_transaction_details: &str = r#" +fragment TransactionDetails on TransactionItem { + id + actions + externalUrl + message + authorizedAmount { + currency + amount + } + authorizePendingAmount { + currency + amount + } + canceledAmount { + currency + amount + } + cancelPendingAmount { + currency + amount + } + chargedAmount { + currency + amount + } + chargePendingAmount { + currency + amount + } + refundedAmount { + currency + amount + } +} +"#; + +pub const fragment_order_details: &str = r#" +fragment OrderDetails on Order { + checkoutId + id + status + isPaid + paymentStatus + chargeStatus + canFinalize + totalBalance { + currency + amount + } +} +"#; + +pub const sub_payment_gateway_initialize_session: &str = concatcp!( + r#" +subscription PaymentGatewayInitializeSession { + event { + ... on PaymentGatewayInitializeSession { + data + amount + sourceObject { + ...OrderDetails + } + amount + } + } +} +"#, + fragment_order_details +); + +pub const sub_transaction_initialize_session: &str = concatcp!( + r#" +subscription transactionInitializeSession { + event { + ... on TransactionInitializeSession { + data + sourceObject { + ...OrderDetails + } + transaction { + ...TransactionDetails + } + action { + amount + currency + actionType + } + } + } +} +"#, + fragment_order_details, + fragment_transaction_details +); + +pub const sub_transaction_process_session: &str = concatcp!( + r#" +subscription transactionProcessSession { + event { + ... on TransactionProcessSession { + action { + amount + actionType + } + sourceObject { + ...OrderDetails + } + transaction { + ...TransactionDetails + } + data + } + } +} +"#, + fragment_order_details, + fragment_transaction_details +); + +pub const sub_transaction_charge_requested: &str = concatcp!( + r#" +subscription transactionChargeRequested { + event { + ... on TransactionChargeRequested { + action { + amount + actionType + } + transaction { + ...TransactionDetails + } + } + } +} +"#, + fragment_transaction_details +); + +pub const sub_transaction_refund_requested: &str = concatcp!( + r#" +subscription transactionRefundRequested { + event { + ... on TransactionRefundRequested { + action { + amount + actionType + } + transaction { + ...TransactionDetails + } + } + } +} +"#, + fragment_transaction_details +); + +pub const sub_transaction_cancelation_requested: &str = concatcp!( + r#" +subscription transactionCancelationRequested { + event { + ... on TransactionCancelationRequested { + action { + amount + actionType + } + transaction { + ...TransactionDetails + } + } + } +} +"#, + fragment_transaction_details +); diff --git a/simple-payment-gateway/src/queries/mod.rs b/simple-payment-gateway/src/queries/mod.rs new file mode 100644 index 0000000..41f3df2 --- /dev/null +++ b/simple-payment-gateway/src/queries/mod.rs @@ -0,0 +1,3 @@ +pub mod event_transactions; +pub mod mutation_transaction_update; + diff --git a/simple-payment-gateway/src/queries/mutation_transaction_update.rs b/simple-payment-gateway/src/queries/mutation_transaction_update.rs new file mode 100644 index 0000000..26499c6 --- /dev/null +++ b/simple-payment-gateway/src/queries/mutation_transaction_update.rs @@ -0,0 +1,17 @@ +/* +mutation transactionUpdate($id: ID!, $transaction: TransactionUpdateInput) { + transactionUpdate(id: $id, transaction: $transaction) { + transaction { + id + actions + externalUrl + message + } + errors { + field + message + code + } + } +} +*/ diff --git a/simple-payment-gateway/src/routes/manifest.rs b/simple-payment-gateway/src/routes/manifest.rs new file mode 100644 index 0000000..3ab361c --- /dev/null +++ b/simple-payment-gateway/src/routes/manifest.rs @@ -0,0 +1,8 @@ +use axum::{extract::State, Json}; +use saleor_app_sdk::{manifest::AppManifest}; + +use crate::app::{AppError, AppState}; + +pub async fn manifest(State(state): State) -> Result, AppError> { + Ok(Json(state.manifest)) +} diff --git a/simple-payment-gateway/src/routes/mod.rs b/simple-payment-gateway/src/routes/mod.rs new file mode 100644 index 0000000..5124453 --- /dev/null +++ b/simple-payment-gateway/src/routes/mod.rs @@ -0,0 +1,40 @@ +use axum::{ + handler::HandlerWithoutStateExt, + http::StatusCode, + middleware, + routing::{get, post}, + Router, +}; +use saleor_app_sdk::middleware::verify_webhook_signature::webhook_signature_verifier; +use tower_http::services::ServeDir; + +use crate::app::AppState; + +pub mod manifest; +pub mod register; +pub mod webhooks; +use manifest::manifest; +use register::register; +use webhooks::webhooks; + +pub fn create_routes(state: AppState) -> Router { + async fn handle_404() -> (StatusCode, &'static str) { + (StatusCode::NOT_FOUND, "Not found") + } + let service = handle_404.into_service(); + let serve_dir = ServeDir::new("saleor-app-template/public").not_found_service(service); + + Router::new() + .layer(middleware::from_fn(webhook_signature_verifier)) + //handles just path, eg. localhost:3000/ + .route("/api/webhooks", post(webhooks)) + .route( + "/", + get(|| async { "Your app got installed successfully!" }), + ) + //handles files, eg. localhost:3000/logo.png + .fallback_service(serve_dir) + .route("/api/manifest", get(manifest)) + .route("/api/register", post(register)) + .with_state(state) +} diff --git a/simple-payment-gateway/src/routes/register.rs b/simple-payment-gateway/src/routes/register.rs new file mode 100644 index 0000000..2203984 --- /dev/null +++ b/simple-payment-gateway/src/routes/register.rs @@ -0,0 +1,40 @@ +use anyhow::Context; +use axum::{ + extract::Json, + extract::State, + http::{HeaderMap, StatusCode}, +}; +use saleor_app_sdk::{AuthData, AuthToken}; +use tracing::{debug, info}; + +use crate::app::{AppError, AppState}; + +pub async fn register( + headers: HeaderMap, + State(state): State, + Json(auth_token): Json, +) -> Result { + debug!( + "/api/register:\nsaleor_api_url:{:?}\nauth_token:{:?}", + headers.get("saleor-api-url"), + auth_token + ); + + if auth_token.auth_token.is_empty() { + return Err(anyhow::anyhow!("missing auth_token").into()); + } + let app = state.saleor_app.lock().await; + let saleor_api_url = headers.get("saleor-api-url").context("missing api field")?; + let saleor_api_url = saleor_api_url.to_str()?.to_owned(); + let auth_data = AuthData { + jwks: None, + token: auth_token.auth_token, + domain: Some(state.config.app_api_base_url), + app_id: state.manifest.id, + saleor_api_url: saleor_api_url.clone(), + }; + app.apl.set(auth_data).await?; + + info!("registered app for{:?}", &saleor_api_url); + Ok(StatusCode::OK) +} diff --git a/simple-payment-gateway/src/routes/webhooks.rs b/simple-payment-gateway/src/routes/webhooks.rs new file mode 100644 index 0000000..ec70877 --- /dev/null +++ b/simple-payment-gateway/src/routes/webhooks.rs @@ -0,0 +1,58 @@ +use anyhow::Context; +use axum::{ + extract::State, + http::{HeaderMap, StatusCode}, +}; +use saleor_app_sdk::{ + headers::SALEOR_API_URL_HEADER, + webhooks::{ + utils::{get_webhook_event_type, EitherWebhookType}, + SyncWebhookEventType, + }, +}; +use tracing::{debug, error, info}; + +use crate::app::{AppError, AppState}; + +pub async fn webhooks( + headers: HeaderMap, + State(state): State, + body: String, +) -> Result { + debug!("/api/webhooks"); + debug!("req: {:?}", body); + debug!("headers: {:?}", headers); + + let url = headers + .get(SALEOR_API_URL_HEADER) + .context("missing saleor api url header")? + .to_str()? + .to_owned(); + let event_type = get_webhook_event_type(&headers)?; + match event_type { + EitherWebhookType::Sync(a) => match a { + SyncWebhookEventType::PaymentGatewayInitializeSession => { + initialize_gateway(&state, &url).await?; + } + SyncWebhookEventType::TransactionProcessSession + | SyncWebhookEventType::TransactionChargeRequested + | SyncWebhookEventType::TransactionRefundRequested + | SyncWebhookEventType::TransactionInitializeSession => { + update_transaction_response(&state, &url).await?; + } + _ => (), + }, + _ => (), + } + + info!("got webhooks!"); + Ok(StatusCode::OK) +} + +async fn initialize_gateway(state: &AppState, saleor_api_url: &str) -> anyhow::Result<()> { + todo!() +} + +async fn update_transaction_response(state: &AppState, saleor_api_url: &str) -> anyhow::Result<()> { + todo!() +} diff --git a/sitemap-generator/Cargo.toml b/sitemap-generator/Cargo.toml index 3b47b2c..1bd2785 100644 --- a/sitemap-generator/Cargo.toml +++ b/sitemap-generator/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/djkato/saleor-apps-rs" documentation = "https://github.com/djkato/saleor-apps-rs" keywords = ["saleor", "plugin"] categories = ["web-programming::http-server"] -license = "PolyForm Noncommercial License 1.0.0" +license = "PolyForm-Noncommercial-1.0.0" [dependencies] anyhow.workspace = true diff --git a/sitemap-generator/public/logo.png b/sitemap-generator/public/logo.png index f591130..224be5b 100644 Binary files a/sitemap-generator/public/logo.png and b/sitemap-generator/public/logo.png differ