Mailer and payment
This commit is contained in:
parent
7634d4ccb1
commit
4943e370b6
11
.env
11
.env
@ -4,8 +4,9 @@ RUST_LOG=debug
|
||||
KEY_SECRET="NEPJs#8jjn8SK8GC7QEC^*P844UgsyEbQB8mRWXkT%3mPrwewZoc25MMby9H#R*w2KzaQgMkk#Pif$kxrLy*N5L!Ch%jxbWoa%gb"
|
||||
JWT_SECRET="42^iFq&ZnQbUf!hwGWXd&CpyY6QQyJmkPU%esFCvne5&Ejcb3nJ4&GyHZp!MArZLf^9*5c6!!VgM$iZ8T%d#&bWTi&xbZk2S@4RN"
|
||||
PGDATESTYLE=
|
||||
SENDMAIL_SECRET=1a9dd7ba41edc5fd030e2281c52b7b49-02fa25a3-02e6d387
|
||||
SENDMAIL_KEY_ID=02fa25a3-02e6d387
|
||||
SMTP_HOST=smtp.mailgun.org
|
||||
SMTP_USER=bazzar@sandbox69e5633f64264d8c973b3c82408dfb17.mailgun.org
|
||||
SMTP_PASSWORD=49940d86d0742959f677c74cc542abb0-02fa25a3-f65fc7b4
|
||||
|
||||
SENDGRID_SECRET=SG.CUWRM-eoQfGJNqSU2bbwkg.NW5aBy5vZueCSOwIIyWUBqq5USChGiwAFrWzreBKvOU
|
||||
SENDGRID_API_KEY=CUWRM-eoQfGJNqSU2bbwkg
|
||||
SMTP_FROM=adrian.wozniak@ita-prog.pl
|
||||
|
||||
PAY_U_BEARER=d9a4536e-62ba-4f60-8017-6053211d3f47
|
||||
|
283
Cargo.lock
generated
283
Cargo.lock
generated
@ -609,13 +609,13 @@ dependencies = [
|
||||
"hmac",
|
||||
"jemallocator",
|
||||
"jwt",
|
||||
"lettre",
|
||||
"log",
|
||||
"oauth2",
|
||||
"parking_lot 0.12.0",
|
||||
"password-hash",
|
||||
"pretty_env_logger",
|
||||
"rand_core",
|
||||
"sendgrid",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.2",
|
||||
@ -815,6 +815,12 @@ dependencies = [
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chunked_transfer"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.3.0"
|
||||
@ -854,6 +860,22 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.2"
|
||||
@ -961,6 +983,12 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
@ -1047,15 +1075,6 @@ version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "email-encoding"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6690291166824e467790ac08ba42f241791567e8337bbf00c5a6e87889629f98"
|
||||
dependencies = [
|
||||
"base64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.31"
|
||||
@ -1123,6 +1142,21 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.0.1"
|
||||
@ -1417,17 +1451,6 @@ dependencies = [
|
||||
"digest 0.10.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"match_cfg",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.6"
|
||||
@ -1520,6 +1543,19 @@ dependencies = [
|
||||
"tokio-rustls 0.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"hyper",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
@ -1661,34 +1697,6 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.10.0-rc.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5144148f337be14dabfc0f0d85b691a68ac6c77ef22a5c47c5504b70a7c9fcf3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"email-encoding",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hostname",
|
||||
"httpdate",
|
||||
"idna",
|
||||
"mime",
|
||||
"nom",
|
||||
"once_cell",
|
||||
"quoted_printable",
|
||||
"regex",
|
||||
"rustls 0.20.4",
|
||||
"rustls-pemfile",
|
||||
"tokio",
|
||||
"tokio-rustls 0.23.3",
|
||||
"tracing",
|
||||
"webpki-roots 0.22.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.123"
|
||||
@ -1772,12 +1780,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||
|
||||
[[package]]
|
||||
name = "match_cfg"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.9"
|
||||
@ -1862,6 +1864,24 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.23.1"
|
||||
@ -1981,6 +2001,39 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry"
|
||||
version = "0.17.0"
|
||||
@ -2085,6 +2138,18 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc"
|
||||
|
||||
[[package]]
|
||||
name = "pay_u"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
@ -2269,12 +2334,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quoted_printable"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@ -2381,6 +2440,15 @@ version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.10"
|
||||
@ -2397,11 +2465,13 @@ dependencies = [
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls 0.20.4",
|
||||
@ -2410,6 +2480,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.23.3",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@ -2512,6 +2583,16 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
@ -2538,6 +2619,29 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.9.0"
|
||||
@ -2559,6 +2663,20 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
|
||||
[[package]]
|
||||
name = "sendgrid"
|
||||
version = "0.17.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88afc7511b68247bf469e0dff55eb7ef305aab293211242695fd07cc17e36a78"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.136"
|
||||
@ -2926,6 +3044,20 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tera"
|
||||
version = "1.15.0"
|
||||
@ -3099,6 +3231,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.22.0"
|
||||
@ -3359,6 +3501,25 @@ version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chunked_transfer",
|
||||
"flate2",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.20.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"webpki 0.22.0",
|
||||
"webpki-roots 0.22.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.2.2"
|
||||
|
@ -1,2 +1,2 @@
|
||||
[workspace]
|
||||
members = ["api"]
|
||||
members = ["api","pay_u"]
|
||||
|
@ -63,4 +63,4 @@ async-trait = { version = "0.1.53", features = [] }
|
||||
|
||||
jemallocator = { version = "0.3.2", features = [] }
|
||||
|
||||
lettre = { version = "0.10.0-rc.5", default-features = false, features = ["tracing", "smtp-transport", "hostname", "builder", "tokio1-rustls-tls"] }
|
||||
sendgrid = { version = "0.17.4", features = ["async"] }
|
||||
|
@ -1,13 +1,36 @@
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{AsyncSmtpTransport, Tokio1Executor};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Email;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! mail_async_handler {
|
||||
($msg: ty, $async: ident, $res: ty) => {
|
||||
impl actix::Handler<$msg> for EmailManager {
|
||||
type Result = actix::ResponseActFuture<Self, Result<$res>>;
|
||||
|
||||
fn handle(&mut self, msg: $msg, _ctx: &mut Self::Context) -> Self::Result {
|
||||
use actix::WrapFuture;
|
||||
let inner = self.0.clone();
|
||||
Box::pin(async { $async(msg, inner).await }.into_actor(self))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub struct EmailManager {
|
||||
mailer: AsyncSmtpTransport<Tokio1Executor>,
|
||||
pub struct SendState {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
pub struct EmailManager(Arc<Inner>);
|
||||
|
||||
pub(crate) struct Inner {
|
||||
from: Email,
|
||||
send_grid: sendgrid::SGClient,
|
||||
}
|
||||
|
||||
impl actix::Actor for EmailManager {
|
||||
@ -16,17 +39,41 @@ impl actix::Actor for EmailManager {
|
||||
|
||||
impl EmailManager {
|
||||
pub fn build() -> Result<Self> {
|
||||
let smtp_credentials = Credentials::new(
|
||||
std::env::var("SMTP_USER").expect("Missing SMTP_USER variable"),
|
||||
std::env::var("SMTP_PASSWORD").expect("Missing SMTP_PASSWORD variable"),
|
||||
);
|
||||
let from = std::env::var("SMTP_FROM").expect("Missing SMTP_FROM variable");
|
||||
|
||||
let mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(
|
||||
&std::env::var("SMTP_USER").expect("Missing SMTP_USER env variable"),
|
||||
Ok(Self(Arc::new(Inner {
|
||||
from: Email::from(from),
|
||||
send_grid: sendgrid::SGClient::new(
|
||||
std::env::var("SENDGRID_SECRET").expect("Missing SENDGRID_SECRET variable"),
|
||||
),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(actix::Message)]
|
||||
#[rtype(result = "Result<SendState>")]
|
||||
pub struct TestMail {
|
||||
pub receiver: Email,
|
||||
}
|
||||
|
||||
mail_async_handler!(TestMail, test_mail, SendState);
|
||||
|
||||
pub(crate) async fn test_mail(msg: TestMail, inner: Arc<Inner>) -> Result<SendState> {
|
||||
let from: &str = &*inner.from;
|
||||
let status = inner
|
||||
.send_grid
|
||||
.send(
|
||||
sendgrid::Mail::new()
|
||||
.add_to((msg.receiver.as_str(), "User").into())
|
||||
.add_from(from)
|
||||
.add_subject("Test e-mail")
|
||||
.add_html("<h1>Test e-mail</h1>")
|
||||
.build(),
|
||||
)
|
||||
.expect("Can't construct async smtp mailer")
|
||||
.credentials(smtp_credentials)
|
||||
.build();
|
||||
Ok(Self { mailer })
|
||||
}
|
||||
.await;
|
||||
|
||||
log::debug!("{:?}", status);
|
||||
Ok(SendState {
|
||||
success: status.is_ok(),
|
||||
})
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ use jemallocator::Jemalloc;
|
||||
use password_hash::SaltString;
|
||||
use validator::{validate_email, validate_length};
|
||||
|
||||
use crate::actors::{database, order_manager, token_manager};
|
||||
use crate::actors::{database, email_manager, order_manager, token_manager};
|
||||
use crate::email_manager::TestMail;
|
||||
use crate::logic::encrypt_password;
|
||||
use crate::model::{Email, Login, PassHash, Password, Role};
|
||||
|
||||
@ -66,6 +67,8 @@ enum Command {
|
||||
GenerateHash(GenerateHashOpts),
|
||||
#[options(help = "Create new account")]
|
||||
CreateAccount(CreateAccountOpts),
|
||||
#[options(help = "Check mailer config")]
|
||||
TestMailer(TestMailerOpts),
|
||||
}
|
||||
|
||||
impl Default for Command {
|
||||
@ -104,6 +107,13 @@ impl ResolveDbUrl for ServerOpts {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Options, Debug)]
|
||||
pub struct TestMailerOpts {
|
||||
help: bool,
|
||||
#[options(help = "E-mail receiver")]
|
||||
receiver: Option<Email>,
|
||||
}
|
||||
|
||||
#[derive(Options, Debug)]
|
||||
struct MigrateOpts {
|
||||
help: bool,
|
||||
@ -270,6 +280,27 @@ async fn create_account(opts: CreateAccountOpts) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_mailer(opts: TestMailerOpts) -> Result<()> {
|
||||
let manager = email_manager::EmailManager::build()
|
||||
.expect("Invalid email manager config")
|
||||
.start();
|
||||
if manager
|
||||
.send(TestMail {
|
||||
receiver: opts.receiver.expect("e-mail address is required"),
|
||||
})
|
||||
.await
|
||||
.expect("Failed to execute actor")
|
||||
.expect("Failed to send email")
|
||||
.success
|
||||
{
|
||||
println!("Success!");
|
||||
} else {
|
||||
eprintln!("Failure!");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
@ -281,5 +312,6 @@ async fn main() -> Result<()> {
|
||||
Command::Server(opts) => server(opts).await,
|
||||
Command::GenerateHash(opts) => generate_hash(opts).await,
|
||||
Command::CreateAccount(opts) => create_account(opts).await,
|
||||
Command::TestMailer(opts) => test_mailer(opts).await,
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
use std::fmt::Formatter;
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_more::{Deref, Display, From};
|
||||
use serde::de::{Error, Visitor};
|
||||
@ -10,6 +11,8 @@ pub enum TransformError {
|
||||
BelowMinimal,
|
||||
#[error("Given value is not valid day flag")]
|
||||
NotDay,
|
||||
#[error("Given value is not valid email address")]
|
||||
NotEmail,
|
||||
}
|
||||
|
||||
pub type RecordId = i32;
|
||||
@ -156,6 +159,18 @@ pub struct Login(String);
|
||||
#[serde(transparent)]
|
||||
pub struct Email(String);
|
||||
|
||||
impl FromStr for Email {
|
||||
type Err = TransformError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if validator::validate_email(s) {
|
||||
Ok(Self(String::from(s)))
|
||||
} else {
|
||||
Err(TransformError::NotEmail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Email {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
17
pay_u/Cargo.toml
Normal file
17
pay_u/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "pay_u"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ureq = { version = "2.4.0", features = ["json", "serde_json"] }
|
||||
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = { version = "1.0.79" }
|
||||
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
|
||||
thiserror = { version = "1.0.30" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.17.0", features = ["full"] }
|
324
pay_u/src/lib.rs
Normal file
324
pay_u/src/lib.rs
Normal file
@ -0,0 +1,324 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Client is not authorized. No bearer token available")]
|
||||
NoToken,
|
||||
#[error("{0}")]
|
||||
UReq(#[from] ureq::Error),
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Total value is not sum of products price")]
|
||||
IncorrectTotal,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// PayU payment status.
|
||||
///
|
||||
/// Each payment is initially Pending and can change according to following
|
||||
/// graph:
|
||||
///
|
||||
/// <img src="https://developers.payu.com/images/order_statusesV2-en.png">
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PaymentStatus {
|
||||
/// Payment is currently being processed.
|
||||
Pending,
|
||||
/// PayU is currently waiting for the merchant system to receive (capture)
|
||||
/// the payment. This status is set if auto-receive is disabled on the
|
||||
/// merchant system.
|
||||
WaitingForConfirmation,
|
||||
/// Payment has been accepted. PayU will pay out the funds shortly.
|
||||
Completed,
|
||||
/// Payment has been cancelled and the buyer has not been charged (no money
|
||||
/// was taken from buyer's account).
|
||||
Canceled,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Buyer {
|
||||
/// Required customer e-mail
|
||||
pub email: String,
|
||||
/// Required customer phone number
|
||||
pub phone: String,
|
||||
/// Required customer first name
|
||||
pub first_name: String,
|
||||
/// Required customer last name
|
||||
pub last_name: String,
|
||||
/// Required customer language
|
||||
pub language: String,
|
||||
}
|
||||
|
||||
impl Buyer {
|
||||
pub fn new<Email, Phone, FirstName, LastName, Language>(
|
||||
email: Email,
|
||||
phone: Phone,
|
||||
first_name: FirstName,
|
||||
last_name: LastName,
|
||||
lang: Language,
|
||||
) -> Self
|
||||
where
|
||||
Email: Into<String>,
|
||||
Phone: Into<String>,
|
||||
FirstName: Into<String>,
|
||||
LastName: Into<String>,
|
||||
Language: Into<String>,
|
||||
{
|
||||
Self {
|
||||
email: email.into(),
|
||||
phone: phone.into(),
|
||||
first_name: first_name.into(),
|
||||
last_name: last_name.into(),
|
||||
language: lang.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Price = i32;
|
||||
pub type Quantity = u32;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Product {
|
||||
pub name: String,
|
||||
#[serde(serialize_with = "Product::serialize_unit_price")]
|
||||
pub unit_price: Price,
|
||||
#[serde(serialize_with = "Product::serialize_quantity")]
|
||||
pub quantity: Quantity,
|
||||
}
|
||||
|
||||
impl Product {
|
||||
pub fn new<Name: Into<String>>(name: Name, unit_price: Price, quantity: Quantity) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
unit_price,
|
||||
quantity,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_unit_price<S>(v: &Price, ser: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
ser.serialize_str(&format!("{v}"))
|
||||
}
|
||||
|
||||
fn serialize_quantity<S>(v: &Quantity, ser: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
ser.serialize_str(&format!("{v}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Order {
|
||||
/// URL to which web hook will be send. It's important to return 200 to all
|
||||
/// notifications.
|
||||
///
|
||||
/// All notifications are send as POST with JSON payload
|
||||
///
|
||||
/// Notifications are sent immediately after a payment status changes. If
|
||||
/// the notification is not received by the Shop application, it will be
|
||||
/// sent again in accordance with the table below:
|
||||
///
|
||||
/// | Attempt | Time |
|
||||
/// |---------|------|
|
||||
/// | 1 | immediately |
|
||||
/// | 2 | 1 minute |
|
||||
/// | 3 | 2 minutes |
|
||||
/// | 4 | 5 minutes |
|
||||
/// | 5 | 10 minutes |
|
||||
/// | 6 | 30 minutes |
|
||||
/// | 7 | 1 hour |
|
||||
/// | 8 | 2 hours |
|
||||
/// | 9 | 3 hours |
|
||||
/// | 10| 6 hours |
|
||||
/// | 11| 9 hours |
|
||||
/// | 12| 12 hours |
|
||||
/// | 13| 15 hours |
|
||||
/// | 14| 18 hours |
|
||||
/// | 15| 21 hours |
|
||||
/// | 16| 24 hours |
|
||||
/// | 17| 36 hours |
|
||||
/// | 18| 48 hours |
|
||||
/// | 19| 60 hours |
|
||||
/// | 20| 72 hours |
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub notify_url: Option<String>,
|
||||
/// Customer client IP address
|
||||
pub customer_ip: String,
|
||||
/// Secret pos ip. This is connected to PayU account
|
||||
pub merchant_pos_ip: String,
|
||||
/// Transaction description
|
||||
pub description: String,
|
||||
/// 3 characters currency identifier, ex. PLN
|
||||
pub currency_code: String,
|
||||
/// Total price of the order in pennies (e.g. 1000 is 10.00 EUR). Applies
|
||||
/// also to currencies without subunits (e.g. 1000 is 10 HUF).
|
||||
#[serde(serialize_with = "Order::serialize_total_amount")]
|
||||
pub total_amount: Price,
|
||||
/// @see [Buyer]
|
||||
pub buyer: Buyer,
|
||||
/// List of products
|
||||
pub products: Vec<Product>,
|
||||
#[serde(skip_serializing)]
|
||||
pub order_create_date: Option<String>,
|
||||
}
|
||||
|
||||
impl Order {
|
||||
fn serialize_total_amount<S>(v: &Price, ser: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
ser.serialize_str(&format!("{v}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PaymentType {
|
||||
Pbl,
|
||||
CartToken,
|
||||
Installments,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PayMethod {
|
||||
#[serde(rename = "type")]
|
||||
pub payment_type: PaymentType,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Status {
|
||||
pub status_code: String,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn is_success(&self) -> bool {
|
||||
self.status_code.as_str() == "SUCCESS"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateOrderResult {
|
||||
pub status: Status,
|
||||
/// Client should be redirected to this URI
|
||||
pub redirect_uri: String,
|
||||
/// This should be connected to your own order
|
||||
pub order_id: String,
|
||||
/// This is YOUR_EXT_ORDER_ID
|
||||
pub ext_order_id: String,
|
||||
}
|
||||
|
||||
pub mod notify {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StatusUpdate {
|
||||
pub order: Order,
|
||||
pub local_receipt_date_time: Option<String>,
|
||||
pub properties: Option<Vec<Prop>>,
|
||||
}
|
||||
|
||||
impl StatusUpdate {
|
||||
pub fn status(&self) -> super::PaymentStatus {
|
||||
self.order.status
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Order {
|
||||
#[serde(flatten)]
|
||||
pub order: super::Order,
|
||||
pub pay_method: Option<super::PaymentType>,
|
||||
pub status: super::PaymentStatus,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Prop {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
pub bearer: Option<String>,
|
||||
pub sandbox: bool,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn create_order(&self, order: Order) -> Result<CreateOrderResult> {
|
||||
println!("{}", serde_json::to_string_pretty(&order).unwrap());
|
||||
if order.total_amount
|
||||
!= order
|
||||
.products
|
||||
.iter()
|
||||
.fold(0, |memo, p| memo + (p.unit_price * p.quantity as i32))
|
||||
{
|
||||
return Err(Error::IncorrectTotal);
|
||||
}
|
||||
|
||||
let req = ureq::post(&format!("{}/orders", self.base_url()))
|
||||
.set("Content-Type", "application/json")
|
||||
.set(
|
||||
"Authorization",
|
||||
&self
|
||||
.bearer
|
||||
.as_ref()
|
||||
.map(|s| format!("Bearer {s}"))
|
||||
.ok_or(Error::NoToken)?,
|
||||
);
|
||||
eprintln!("{:?}", req);
|
||||
let res = req.send_string(&serde_json::to_string_pretty(&order).unwrap())?;
|
||||
res.into_json::<CreateOrderResult>().map_err(Into::into)
|
||||
}
|
||||
|
||||
fn base_url(&self) -> &str {
|
||||
if self.sandbox {
|
||||
"https://secure.snd.payu.com/api/v2_1"
|
||||
} else {
|
||||
"https://secure.payu.com/api/v2_1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokio::test;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
async fn create_order() {
|
||||
let client = Client {
|
||||
bearer: Some(String::from("d9a4536e-62ba-4f60-8017-6053211d3f47")),
|
||||
sandbox: true,
|
||||
};
|
||||
let res = client.create_order(Order {
|
||||
notify_url: Some(String::from("https://your.eshop.com/notify")),
|
||||
customer_ip: "127.0.0.1".to_string(),
|
||||
merchant_pos_ip: "300746".to_string(),
|
||||
description: "RTV market".to_string(),
|
||||
currency_code: "PLN".to_string(),
|
||||
total_amount: 21000,
|
||||
buyer: Buyer::new("john.doe@example.com", "654111654", "John", "Doe", "pl"),
|
||||
products: vec![
|
||||
Product::new("Wireless Mouse for Laptop", 15000, 1),
|
||||
Product::new("HDMI cable", 6000, 1),
|
||||
],
|
||||
order_create_date: None,
|
||||
});
|
||||
eprintln!("{:#?}", res);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user