before adding GH CI

This commit is contained in:
Djkáťo 2024-03-11 14:11:47 +01:00
parent ee34b1bf22
commit f6b1977057
23 changed files with 597 additions and 64 deletions

35
.github/rust.yml vendored
View file

@ -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

46
Cargo.lock generated
View file

@ -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"

View file

@ -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 <project-name>`, then `cp saleor-app-template/* <project-name>`.
If using the `saleor-app-template`, create a new workspace member `cargo new <project-name>`,`rm -rf <project-name>/*` then `cp -r app-template/* <project-name>/`.
## Adding new dependencies

View file

@ -1 +0,0 @@
To update the schema, you can download it from https://raw.githubusercontent.com/saleor/saleor/main/saleor/graphql/schema.graphql

View file

@ -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<String> = 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!()
}
}
*/

View file

@ -18,6 +18,7 @@ pub enum AppPermission {
ManageGiftCard,
ManageMenus,
ManageOrders,
ManageOrdersImport,
ManagePages,
ManagePageTypesAndAttributes,
HandlePayments,

View file

@ -2,5 +2,38 @@
name = "simple-payment-gateway"
version = "0.1.0"
edition = "2021"
authors = ["Djkáťo <djkatovfx@gmail.com>"]
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

View file

@ -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"]

View file

@ -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/

View file

@ -0,0 +1,7 @@
fn main() {
cynic_codegen::register_schema("saleor")
.from_sdl_file("schema/schema.graphql")
.unwrap()
.as_default()
.unwrap();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -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<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
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<tokio::sync::Mutex<SaleorApp>>,
pub config: Config,
pub manifest: AppManifest,
}

View file

@ -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::<Vec<_>>()
.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),
}
}

View file

@ -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
);

View file

@ -0,0 +1,3 @@
pub mod event_transactions;
pub mod mutation_transaction_update;

View file

@ -0,0 +1,17 @@
/*
mutation transactionUpdate($id: ID!, $transaction: TransactionUpdateInput) {
transactionUpdate(id: $id, transaction: $transaction) {
transaction {
id
actions
externalUrl
message
}
errors {
field
message
code
}
}
}
*/

View file

@ -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<AppState>) -> Result<Json<AppManifest>, AppError> {
Ok(Json(state.manifest))
}

View file

@ -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)
}

View file

@ -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<AppState>,
Json(auth_token): Json<AuthToken>,
) -> Result<StatusCode, AppError> {
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)
}

View file

@ -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<AppState>,
body: String,
) -> Result<StatusCode, AppError> {
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!()
}

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 11 KiB