mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 04:56:49 +00:00
feat(premium): initial subscription implementation through paddle
This commit is contained in:
parent
81cde5e688
commit
226947e6aa
15 changed files with 1121 additions and 144 deletions
413
Cargo.lock
generated
413
Cargo.lock
generated
|
|
@ -90,7 +90,7 @@ dependencies = [
|
||||||
"metrics",
|
"metrics",
|
||||||
"pk_macros",
|
"pk_macros",
|
||||||
"pluralkit_models",
|
"pluralkit_models",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.28",
|
||||||
"reverse-proxy-service",
|
"reverse-proxy-service",
|
||||||
"sea-query",
|
"sea-query",
|
||||||
"sea-query-sqlx",
|
"sea-query-sqlx",
|
||||||
|
|
@ -101,7 +101,7 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
"tower-http",
|
"tower-http 0.5.2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"twilight-http",
|
"twilight-http",
|
||||||
]
|
]
|
||||||
|
|
@ -240,7 +240,7 @@ dependencies = [
|
||||||
"gif",
|
"gif",
|
||||||
"image",
|
"image",
|
||||||
"libpk",
|
"libpk",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.28",
|
||||||
"rust-s3",
|
"rust-s3",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|
@ -894,8 +894,18 @@ version = "0.20.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.20.11",
|
||||||
"darling_macro",
|
"darling_macro 0.20.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.21.3",
|
||||||
|
"darling_macro 0.21.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -911,13 +921,38 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.20.11"
|
version = "0.20.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.20.11",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.21.3",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
@ -1013,7 +1048,7 @@ dependencies = [
|
||||||
"axum 0.8.4",
|
"axum 0.8.4",
|
||||||
"hickory-client",
|
"hickory-client",
|
||||||
"libpk",
|
"libpk",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -1059,6 +1094,12 @@ version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
|
|
@ -1370,7 +1411,7 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libpk",
|
"libpk",
|
||||||
"metrics",
|
"metrics",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_variant",
|
"serde_variant",
|
||||||
|
|
@ -1469,7 +1510,7 @@ dependencies = [
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"indexmap",
|
"indexmap 2.8.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|
@ -1488,7 +1529,7 @@ dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"indexmap",
|
"indexmap 2.8.0",
|
||||||
"slab",
|
"slab",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|
@ -1802,22 +1843,28 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.11"
|
version = "0.1.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
|
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"hyper 1.6.0",
|
"hyper 1.6.0",
|
||||||
|
"ipnet",
|
||||||
"libc",
|
"libc",
|
||||||
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.5.9",
|
"socket2 0.6.0",
|
||||||
|
"system-configuration 0.6.1",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2017,6 +2064,17 @@ dependencies = [
|
||||||
"quick-error",
|
"quick-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "1.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
|
|
@ -2025,6 +2083,7 @@ checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.15.2",
|
"hashbrown 0.15.2",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2055,6 +2114,16 @@ version = "2.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iri-string"
|
||||||
|
version = "0.7.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
|
@ -2343,7 +2412,7 @@ dependencies = [
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.6.0",
|
"hyper 1.6.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"indexmap",
|
"indexmap 2.8.0",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"metrics",
|
"metrics",
|
||||||
"metrics-util",
|
"metrics-util",
|
||||||
|
|
@ -2584,6 +2653,36 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paddle-rust-sdk"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a6177ee650f105756a75e7c10ed29c31b625346433718cc20218180e96507ee"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"hmac",
|
||||||
|
"paddle-rust-sdk-types",
|
||||||
|
"reqwest 0.12.28",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_qs",
|
||||||
|
"serde_with",
|
||||||
|
"sha2",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paddle-rust-sdk-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eade7cb56ac8217b595a2beca5cf2db20cfd0b0a261edb45662dbe9a1d996de5"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -2789,7 +2888,7 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
|
@ -2828,11 +2927,12 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libpk",
|
"libpk",
|
||||||
"metrics",
|
"metrics",
|
||||||
|
"paddle-rust-sdk",
|
||||||
"pk_macros",
|
"pk_macros",
|
||||||
"pluralkit_models",
|
"pluralkit_models",
|
||||||
"postmark",
|
"postmark",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.28",
|
||||||
"sea-query",
|
"sea-query",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -2841,7 +2941,7 @@ dependencies = [
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http 0.5.2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"twilight-http",
|
"twilight-http",
|
||||||
]
|
]
|
||||||
|
|
@ -3078,6 +3178,26 @@ dependencies = [
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ref-cast"
|
||||||
|
version = "1.0.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
|
||||||
|
dependencies = [
|
||||||
|
"ref-cast-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ref-cast-impl"
|
||||||
|
version = "1.0.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
|
|
@ -3131,12 +3251,12 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustls 0.21.12",
|
"rustls 0.21.12",
|
||||||
"rustls-pemfile 1.0.4",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper 0.1.2",
|
"sync_wrapper 0.1.2",
|
||||||
"system-configuration",
|
"system-configuration 0.5.1",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.24.1",
|
"tokio-rustls 0.24.1",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|
@ -3152,31 +3272,31 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.15"
|
version = "0.12.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"h2 0.4.8",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"http-body 1.0.1",
|
"http-body 1.0.1",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper 1.6.0",
|
"hyper 1.6.0",
|
||||||
"hyper-rustls 0.27.5",
|
"hyper-rustls 0.27.5",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"ipnet",
|
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
"once_cell",
|
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
"rustls 0.23.25",
|
"rustls 0.23.25",
|
||||||
"rustls-pemfile 2.2.0",
|
"rustls-native-certs",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -3185,13 +3305,13 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.26.2",
|
"tokio-rustls 0.26.2",
|
||||||
"tower 0.5.2",
|
"tower 0.5.2",
|
||||||
|
"tower-http 0.6.8",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots 0.26.8",
|
"webpki-roots 1.0.4",
|
||||||
"windows-registry",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3435,15 +3555,6 @@ dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-pemfile"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
|
||||||
dependencies = [
|
|
||||||
"rustls-pki-types",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
|
@ -3544,7 +3655,7 @@ dependencies = [
|
||||||
"libpk",
|
"libpk",
|
||||||
"metrics",
|
"metrics",
|
||||||
"num-format",
|
"num-format",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
|
@ -3553,6 +3664,30 @@ dependencies = [
|
||||||
"twilight-http",
|
"twilight-http",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"ref-cast",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"ref-cast",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -3586,7 +3721,7 @@ version = "1.0.0-rc.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "217e9422de35f26c16c5f671fce3c075a65e10322068dbc66078428634af6195"
|
checksum = "217e9422de35f26c16c5f671fce3c075a65e10322068dbc66078428634af6195"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.20.11",
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -3641,7 +3776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a7332159e544e34db06b251b1eda5e546bd90285c3f58d9c8ff8450b484e0da"
|
checksum = "3a7332159e544e34db06b251b1eda5e546bd90285c3f58d9c8ff8450b484e0da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"reqwest 0.12.15",
|
"reqwest 0.12.28",
|
||||||
"rustls 0.23.25",
|
"rustls 0.23.25",
|
||||||
"sentry-backtrace",
|
"sentry-backtrace",
|
||||||
"sentry-contexts",
|
"sentry-contexts",
|
||||||
|
|
@ -3785,15 +3920,16 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.140"
|
version = "1.0.148"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.8.0",
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
"ryu",
|
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3806,6 +3942,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_qs"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
|
|
@ -3847,6 +3994,37 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.8.0",
|
||||||
|
"schemars 0.9.0",
|
||||||
|
"schemars 1.2.0",
|
||||||
|
"serde_core",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.21.3",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha-1"
|
name = "sha-1"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
|
@ -4032,7 +4210,7 @@ dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hashbrown 0.15.2",
|
"hashbrown 0.15.2",
|
||||||
"hashlink 0.10.0",
|
"hashlink 0.10.0",
|
||||||
"indexmap",
|
"indexmap 2.8.0",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -4217,6 +4395,12 @@ dependencies = [
|
||||||
"unicode-properties",
|
"unicode-properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|
@ -4268,7 +4452,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
"system-configuration-sys",
|
"system-configuration-sys 0.5.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.0",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"system-configuration-sys 0.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4281,6 +4476,16 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration-sys"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.19.1"
|
version = "3.19.1"
|
||||||
|
|
@ -4545,7 +4750,7 @@ version = "0.22.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 2.8.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
|
|
@ -4609,6 +4814,24 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.0",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.3.1",
|
||||||
|
"http-body 1.0.1",
|
||||||
|
"iri-string",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tower 0.5.2",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
|
|
@ -5175,6 +5398,15 @@ dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|
@ -5263,7 +5495,7 @@ dependencies = [
|
||||||
"windows-interface",
|
"windows-interface",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
"windows-result",
|
"windows-result",
|
||||||
"windows-strings 0.4.0",
|
"windows-strings",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5296,13 +5528,13 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-registry"
|
name = "windows-registry"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
"windows-result",
|
"windows-result",
|
||||||
"windows-strings 0.3.1",
|
"windows-strings",
|
||||||
"windows-targets 0.53.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5314,15 +5546,6 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-strings"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -5407,29 +5630,13 @@ dependencies = [
|
||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu 0.52.6",
|
"windows_i686_gnu 0.52.6",
|
||||||
"windows_i686_gnullvm 0.52.6",
|
"windows_i686_gnullvm",
|
||||||
"windows_i686_msvc 0.52.6",
|
"windows_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc 0.52.6",
|
"windows_x86_64_msvc 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm 0.53.0",
|
|
||||||
"windows_aarch64_msvc 0.53.0",
|
|
||||||
"windows_i686_gnu 0.53.0",
|
|
||||||
"windows_i686_gnullvm 0.53.0",
|
|
||||||
"windows_i686_msvc 0.53.0",
|
|
||||||
"windows_x86_64_gnu 0.53.0",
|
|
||||||
"windows_x86_64_gnullvm 0.53.0",
|
|
||||||
"windows_x86_64_msvc 0.53.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
|
|
@ -5448,12 +5655,6 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
|
|
@ -5472,12 +5673,6 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
|
|
@ -5496,24 +5691,12 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
|
|
@ -5532,12 +5715,6 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
|
|
@ -5556,12 +5733,6 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
|
|
@ -5580,12 +5751,6 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
|
|
@ -5604,12 +5769,6 @@ version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.53.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
|
|
@ -5774,6 +5933,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zstd-safe"
|
name = "zstd-safe"
|
||||||
version = "7.2.4"
|
version = "7.2.4"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{ApiContext, auth::AuthState, error::fail};
|
use crate::{ApiContext, auth::AuthState, fail};
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension,
|
Extension,
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use sqlx::Postgres;
|
||||||
|
|
||||||
use pluralkit_models::{PKDashView, PKSystem, PKSystemConfig, PrivacyLevel};
|
use pluralkit_models::{PKDashView, PKSystem, PKSystemConfig, PrivacyLevel};
|
||||||
|
|
||||||
use crate::{ApiContext, auth::AuthState, error::fail};
|
use crate::{ApiContext, auth::AuthState, fail};
|
||||||
|
|
||||||
#[api_endpoint]
|
#[api_endpoint]
|
||||||
pub async fn get_system_settings(
|
pub async fn get_system_settings(
|
||||||
|
|
|
||||||
|
|
@ -84,15 +84,14 @@ impl IntoResponse for PKError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! fail {
|
macro_rules! fail {
|
||||||
($($stuff:tt)+) => {{
|
($($stuff:tt)+) => {{
|
||||||
tracing::error!($($stuff)+);
|
tracing::error!($($stuff)+);
|
||||||
return Err(crate::error::GENERIC_SERVER_ERROR);
|
return Err($crate::error::GENERIC_SERVER_ERROR);
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) use fail;
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! fail_html {
|
macro_rules! fail_html {
|
||||||
($($stuff:tt)+) => {{
|
($($stuff:tt)+) => {{
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,13 @@ pub struct ScheduledTasksConfig {
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
pub struct PremiumConfig {
|
pub struct PremiumConfig {
|
||||||
|
pub paddle_webhook_secret: String,
|
||||||
|
pub paddle_api_key: String,
|
||||||
|
pub paddle_client_token: String,
|
||||||
|
pub paddle_price_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_paddle_production: bool,
|
||||||
|
|
||||||
pub postmark_token: String,
|
pub postmark_token: String,
|
||||||
pub from_email: String,
|
pub from_email: String,
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ api = { path = "../api" }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
axum = { workspace = true }
|
axum = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
axum-extra = { workspace = true }
|
axum-extra = { workspace = true }
|
||||||
fred = { workspace = true }
|
fred = { workspace = true }
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
|
|
@ -30,6 +31,6 @@ postmark = { version = "0.11", features = ["reqwest"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
chrono = { workspace = true }
|
paddle-rust-sdk = { version = "0.16.0", default-features = false, features = ["rustls-native-roots"] }
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
10
crates/premium/init.sql
Normal file
10
crates/premium/init.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
create table premium_subscriptions (
|
||||||
|
id serial primary key,
|
||||||
|
provider text not null,
|
||||||
|
provider_id text not null,
|
||||||
|
email text not null,
|
||||||
|
system_id int references systems(id) on delete set null,
|
||||||
|
status text,
|
||||||
|
next_renewal_at text,
|
||||||
|
unique (provider, provider_id)
|
||||||
|
)
|
||||||
|
|
@ -14,7 +14,7 @@ use fred::{
|
||||||
use rand::{Rng, distributions::Alphanumeric};
|
use rand::{Rng, distributions::Alphanumeric};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::web::{render, message};
|
use crate::web::{message, render};
|
||||||
|
|
||||||
const LOGIN_TOKEN_TTL_SECS: i64 = 60 * 10;
|
const LOGIN_TOKEN_TTL_SECS: i64 = 60 * 10;
|
||||||
|
|
||||||
|
|
@ -162,9 +162,12 @@ pub async fn middleware(
|
||||||
refresh_session_cookie(session, response)
|
refresh_session_cookie(session, response)
|
||||||
} else {
|
} else {
|
||||||
return render!(crate::web::Index {
|
return render!(crate::web::Index {
|
||||||
|
base_url: libpk::config.premium().base_url.clone(),
|
||||||
session: None,
|
session: None,
|
||||||
show_login_form: true,
|
show_login_form: true,
|
||||||
message: None,
|
message: None,
|
||||||
|
subscriptions: vec![],
|
||||||
|
paddle: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,17 +188,23 @@ pub async fn middleware(
|
||||||
};
|
};
|
||||||
let Some(email) = form.get("email") else {
|
let Some(email) = form.get("email") else {
|
||||||
return render!(crate::web::Index {
|
return render!(crate::web::Index {
|
||||||
|
base_url: libpk::config.premium().base_url.clone(),
|
||||||
session: None,
|
session: None,
|
||||||
show_login_form: true,
|
show_login_form: true,
|
||||||
message: Some("email field is required".to_string()),
|
message: Some("email field is required".to_string()),
|
||||||
|
subscriptions: vec![],
|
||||||
|
paddle: None,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
let email = email.trim().to_lowercase();
|
let email = email.trim().to_lowercase();
|
||||||
if email.is_empty() {
|
if email.is_empty() {
|
||||||
return render!(crate::web::Index {
|
return render!(crate::web::Index {
|
||||||
|
base_url: libpk::config.premium().base_url.clone(),
|
||||||
session: None,
|
session: None,
|
||||||
show_login_form: true,
|
show_login_form: true,
|
||||||
message: Some("email field is required".to_string()),
|
message: Some("email field is required".to_string()),
|
||||||
|
subscriptions: vec![],
|
||||||
|
paddle: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,9 +246,12 @@ pub async fn middleware(
|
||||||
let token = path.strip_prefix("/login/").unwrap_or("");
|
let token = path.strip_prefix("/login/").unwrap_or("");
|
||||||
if token.is_empty() {
|
if token.is_empty() {
|
||||||
return render!(crate::web::Index {
|
return render!(crate::web::Index {
|
||||||
|
base_url: libpk::config.premium().base_url.clone(),
|
||||||
session: None,
|
session: None,
|
||||||
show_login_form: true,
|
show_login_form: true,
|
||||||
message: Some("invalid login link".to_string()),
|
message: Some("invalid login link".to_string()),
|
||||||
|
subscriptions: vec![],
|
||||||
|
paddle: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,11 +263,14 @@ pub async fn middleware(
|
||||||
|
|
||||||
let Some(email) = email else {
|
let Some(email) = email else {
|
||||||
return render!(crate::web::Index {
|
return render!(crate::web::Index {
|
||||||
|
base_url: libpk::config.premium().base_url.clone(),
|
||||||
session: None,
|
session: None,
|
||||||
show_login_form: true,
|
show_login_form: true,
|
||||||
message: Some(
|
message: Some(
|
||||||
"invalid or expired login link. please request a new one.".to_string()
|
"invalid or expired login link. please request a new one.".to_string()
|
||||||
),
|
),
|
||||||
|
subscriptions: vec![],
|
||||||
|
paddle: None,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -313,6 +328,14 @@ pub async fn middleware(
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
"/cancel" | "/validate-token" => {
|
||||||
|
if let Some(ref session) = session {
|
||||||
|
let response = next.run(request).await;
|
||||||
|
refresh_session_cookie(session, response)
|
||||||
|
} else {
|
||||||
|
Redirect::to("/").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => (axum::http::StatusCode::NOT_FOUND, "404 not found").into_response(),
|
_ => (axum::http::StatusCode::NOT_FOUND, "404 not found").into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
crates/premium/src/error.rs
Normal file
1
crates/premium/src/error.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub use api::error::*;
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Router,
|
Extension, Router,
|
||||||
response::Html,
|
extract::State,
|
||||||
|
response::{Html, IntoResponse, Response},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
use tower_http::{catch_panic::CatchPanicLayer, services::ServeDir};
|
use tower_http::{catch_panic::CatchPanicLayer, services::ServeDir};
|
||||||
|
|
@ -10,21 +11,56 @@ use tracing::info;
|
||||||
use api::{ApiContext, middleware};
|
use api::{ApiContext, middleware};
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod error;
|
||||||
mod mailer;
|
mod mailer;
|
||||||
|
mod paddle;
|
||||||
|
mod system;
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
|
pub use api::fail;
|
||||||
|
|
||||||
|
async fn home_handler(
|
||||||
|
State(ctx): State<ApiContext>,
|
||||||
|
Extension(session): Extension<auth::AuthState>,
|
||||||
|
) -> Response {
|
||||||
|
let subscriptions = match paddle::fetch_subscriptions_for_email(&ctx, &session.email).await {
|
||||||
|
Ok(subs) => subs,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(?err, "failed to fetch subscriptions for {}", session.email);
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Html(
|
||||||
|
web::Index {
|
||||||
|
base_url: libpk::config.premium().base_url.clone(),
|
||||||
|
session: Some(session),
|
||||||
|
show_login_form: false,
|
||||||
|
message: None,
|
||||||
|
subscriptions,
|
||||||
|
paddle: Some(web::PaddleData {
|
||||||
|
client_token: libpk::config.premium().paddle_client_token.clone(),
|
||||||
|
price_id: libpk::config.premium().paddle_price_id.clone(),
|
||||||
|
environment: if libpk::config.premium().is_paddle_production {
|
||||||
|
"production"
|
||||||
|
} else {
|
||||||
|
"sandbox"
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
// this function is manually formatted for easier legibility of route_services
|
// this function is manually formatted for easier legibility of route_services
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
fn router(ctx: ApiContext) -> Router {
|
fn router(ctx: ApiContext) -> Router {
|
||||||
// processed upside down (???) so we have to put middleware at the end
|
// processed upside down (???) so we have to put middleware at the end
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(|Extension(session): Extension<auth::AuthState>| async move {
|
.route("/", get(home_handler))
|
||||||
Html(web::Index {
|
|
||||||
session: Some(session),
|
|
||||||
show_login_form: false,
|
|
||||||
message: None,
|
|
||||||
}.render().unwrap())
|
|
||||||
}))
|
|
||||||
|
|
||||||
.route("/login/{token}", get(|| async {
|
.route("/login/{token}", get(|| async {
|
||||||
"handled in auth middleware"
|
"handled in auth middleware"
|
||||||
|
|
@ -35,8 +71,13 @@ fn router(ctx: ApiContext) -> Router {
|
||||||
.route("/logout", post(|| async {
|
.route("/logout", post(|| async {
|
||||||
"handled in auth middleware"
|
"handled in auth middleware"
|
||||||
}))
|
}))
|
||||||
|
.route("/cancel", get(paddle::cancel_page).post(paddle::cancel))
|
||||||
|
.route("/validate-token", post(system::validate_token))
|
||||||
|
|
||||||
.layer(axum::middleware::from_fn_with_state(ctx.clone(), auth::middleware))
|
.layer(axum::middleware::from_fn_with_state(ctx.clone(), auth::middleware))
|
||||||
|
|
||||||
|
.route("/paddle", post(paddle::webhook))
|
||||||
|
|
||||||
.layer(axum::middleware::from_fn(middleware::logger::logger))
|
.layer(axum::middleware::from_fn(middleware::logger::logger))
|
||||||
.nest_service("/static", ServeDir::new("static"))
|
.nest_service("/static", ServeDir::new("static"))
|
||||||
.layer(CatchPanicLayer::custom(api::util::handle_panic))
|
.layer(CatchPanicLayer::custom(api::util::handle_panic))
|
||||||
|
|
|
||||||
493
crates/premium/src/paddle.rs
Normal file
493
crates/premium/src/paddle.rs
Normal file
|
|
@ -0,0 +1,493 @@
|
||||||
|
use std::{collections::HashSet, vec};
|
||||||
|
|
||||||
|
use api::ApiContext;
|
||||||
|
use askama::Template;
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use paddle_rust_sdk::{
|
||||||
|
Paddle,
|
||||||
|
entities::{Customer, Subscription},
|
||||||
|
enums::{EventData, SubscriptionStatus},
|
||||||
|
webhooks::MaximumVariance,
|
||||||
|
};
|
||||||
|
use pk_macros::api_endpoint;
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::postgres::Postgres;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
use crate::fail;
|
||||||
|
|
||||||
|
// ew
|
||||||
|
fn html_escape(s: &str) -> String {
|
||||||
|
s.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace('\'', "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref PADDLE_CLIENT: Paddle = {
|
||||||
|
let config = libpk::config.premium();
|
||||||
|
let base_url = if config.is_paddle_production {
|
||||||
|
Paddle::PRODUCTION
|
||||||
|
} else {
|
||||||
|
Paddle::SANDBOX
|
||||||
|
};
|
||||||
|
Paddle::new(&config.paddle_api_key, base_url).expect("failed to create paddle client")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_customer(customer_id: &str) -> anyhow::Result<Customer> {
|
||||||
|
let customer = PADDLE_CLIENT.customer_get(customer_id).send().await?;
|
||||||
|
Ok(customer.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUBSCRIPTION_QUERY: &str = r#"
|
||||||
|
select
|
||||||
|
p.id, p.provider, p.provider_id, p.email, p.system_id,
|
||||||
|
s.hid as system_hid, s.name as system_name,
|
||||||
|
p.status, p.next_renewal_at
|
||||||
|
from premium_subscriptions p
|
||||||
|
left join systems s on p.system_id = s.id
|
||||||
|
"#;
|
||||||
|
|
||||||
|
async fn get_subscriptions_by_email(
|
||||||
|
ctx: &ApiContext,
|
||||||
|
email: &str,
|
||||||
|
) -> anyhow::Result<Vec<DbSubscription>> {
|
||||||
|
let query = format!("{} where p.email = $1", SUBSCRIPTION_QUERY);
|
||||||
|
let subs = sqlx::query_as(&query)
|
||||||
|
.bind(email)
|
||||||
|
.fetch_all(&ctx.db)
|
||||||
|
.await?;
|
||||||
|
Ok(subs)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_subscription(
|
||||||
|
ctx: &ApiContext,
|
||||||
|
provider_id: &str,
|
||||||
|
email: &str,
|
||||||
|
) -> anyhow::Result<Option<DbSubscription>> {
|
||||||
|
let query = format!(
|
||||||
|
"{} where p.provider_id = $1 and p.email = $2",
|
||||||
|
SUBSCRIPTION_QUERY
|
||||||
|
);
|
||||||
|
let sub = sqlx::query_as(&query)
|
||||||
|
.bind(provider_id)
|
||||||
|
.bind(email)
|
||||||
|
.fetch_optional(&ctx.db)
|
||||||
|
.await?;
|
||||||
|
Ok(sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
|
||||||
|
pub struct DbSubscription {
|
||||||
|
pub id: i32,
|
||||||
|
pub provider: String,
|
||||||
|
pub provider_id: String,
|
||||||
|
pub email: String,
|
||||||
|
pub system_id: Option<i32>,
|
||||||
|
pub system_hid: Option<String>,
|
||||||
|
pub system_name: Option<String>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
pub next_renewal_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct SubscriptionInfo {
|
||||||
|
pub db: Option<DbSubscription>,
|
||||||
|
pub paddle: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubscriptionInfo {
|
||||||
|
pub fn subscription_id(&self) -> &str {
|
||||||
|
if let Some(paddle) = &self.paddle {
|
||||||
|
paddle.id.as_ref()
|
||||||
|
} else if let Some(db) = &self.db {
|
||||||
|
&db.provider_id
|
||||||
|
} else {
|
||||||
|
"unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status(&self) -> String {
|
||||||
|
if let Some(paddle) = &self.paddle {
|
||||||
|
if let Some(ref scheduled) = paddle.scheduled_change {
|
||||||
|
if matches!(
|
||||||
|
scheduled.action,
|
||||||
|
paddle_rust_sdk::enums::ScheduledChangeAction::Cancel
|
||||||
|
) {
|
||||||
|
return format!("expires {}", scheduled.effective_at.format("%Y-%m-%d"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format!("{:?}", paddle.status).to_lowercase()
|
||||||
|
} else if let Some(db) = &self.db {
|
||||||
|
db.status.clone().unwrap_or_else(|| "unknown".to_string())
|
||||||
|
} else {
|
||||||
|
"unknown".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_renewal(&self) -> String {
|
||||||
|
if let Some(paddle) = &self.paddle {
|
||||||
|
// if subscription is canceled, show next_billed_at as "ends at" date instead of "next renewal"
|
||||||
|
if paddle.scheduled_change.as_ref().is_some_and(|s| {
|
||||||
|
matches!(
|
||||||
|
s.action,
|
||||||
|
paddle_rust_sdk::enums::ScheduledChangeAction::Cancel
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return "-".to_string();
|
||||||
|
}
|
||||||
|
if let Some(next) = paddle.next_billed_at {
|
||||||
|
return next.format("%Y-%m-%d").to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(db) = &self.db {
|
||||||
|
if let Some(next) = &db.next_renewal_at {
|
||||||
|
return next.split('T').next().unwrap_or(next).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"-".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn system_id_display(&self) -> String {
|
||||||
|
if let Some(db) = &self.db {
|
||||||
|
if let Some(hid) = &db.system_hid {
|
||||||
|
if let Some(name) = &db.system_name {
|
||||||
|
// ew, this needs to be fixed
|
||||||
|
let escaped_name = html_escape(name);
|
||||||
|
return format!("{} (<code>{}</code>)", escaped_name, hid);
|
||||||
|
}
|
||||||
|
return format!("<code>{}</code>", hid);
|
||||||
|
}
|
||||||
|
if db.system_id.is_some() {
|
||||||
|
return "unknown system (contact us at billing@pluralkit.me to fix this)"
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
return "not linked".to_string();
|
||||||
|
}
|
||||||
|
"not linked".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_cancellable(&self) -> bool {
|
||||||
|
if let Some(paddle) = &self.paddle {
|
||||||
|
if paddle.scheduled_change.as_ref().is_some_and(|s| {
|
||||||
|
matches!(
|
||||||
|
s.action,
|
||||||
|
paddle_rust_sdk::enums::ScheduledChangeAction::Cancel
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
matches!(
|
||||||
|
paddle.status,
|
||||||
|
SubscriptionStatus::Active | SubscriptionStatus::PastDue
|
||||||
|
)
|
||||||
|
} else if let Some(db) = &self.db {
|
||||||
|
matches!(db.status.as_deref(), Some("active") | Some("past_due"))
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is slightly terrible, but works
|
||||||
|
// the paddle sdk is a mess which does not help
|
||||||
|
pub async fn fetch_subscriptions_for_email(
|
||||||
|
ctx: &ApiContext,
|
||||||
|
email: &str,
|
||||||
|
) -> anyhow::Result<Vec<SubscriptionInfo>> {
|
||||||
|
let db_subs = get_subscriptions_by_email(ctx, email).await?;
|
||||||
|
|
||||||
|
let mut paddle_subs: Vec<Subscription> = Vec::new();
|
||||||
|
|
||||||
|
// there's no method to look up customer by email, so we have to do this nonsense
|
||||||
|
let Some(customer) = PADDLE_CLIENT
|
||||||
|
.customers_list()
|
||||||
|
.emails([email])
|
||||||
|
.send()
|
||||||
|
.next()
|
||||||
|
.await?
|
||||||
|
.and_then(|v| v.data.into_iter().next())
|
||||||
|
else {
|
||||||
|
return Ok(vec![]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// why
|
||||||
|
let mut temp_paddle_for_sub_list = PADDLE_CLIENT.subscriptions_list();
|
||||||
|
let mut subs_pages = temp_paddle_for_sub_list.customer_id([customer.id]).send();
|
||||||
|
while let Some(subs_page) = subs_pages.next().await? {
|
||||||
|
paddle_subs.extend(subs_page.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results: Vec<SubscriptionInfo> = Vec::new();
|
||||||
|
let mut found_ids: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
|
for db_sub in &db_subs {
|
||||||
|
let paddle_match = paddle_subs
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.id.as_ref() == db_sub.provider_id);
|
||||||
|
|
||||||
|
if let Some(paddle) = paddle_match {
|
||||||
|
found_ids.insert(paddle.id.as_ref().to_string());
|
||||||
|
results.push(SubscriptionInfo {
|
||||||
|
db: Some(db_sub.clone()),
|
||||||
|
paddle: Some(paddle.clone()),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
results.push(SubscriptionInfo {
|
||||||
|
db: Some(db_sub.clone()),
|
||||||
|
paddle: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for paddle_sub in paddle_subs {
|
||||||
|
if !found_ids.contains(paddle_sub.id.as_ref()) {
|
||||||
|
results.push(SubscriptionInfo {
|
||||||
|
db: None,
|
||||||
|
paddle: Some(paddle_sub),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: show some error if a sub is only in db/provider but not both
|
||||||
|
|
||||||
|
// todo: we may want to show canceled subscriptions in the future
|
||||||
|
results.retain(|sub| sub.status() != "canceled");
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_subscription(
|
||||||
|
ctx: &ApiContext,
|
||||||
|
sub: &Subscription,
|
||||||
|
email: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let status = format!("{:?}", sub.status).to_lowercase();
|
||||||
|
let next_renewal_at = sub.next_billed_at.map(|dt| dt.to_rfc3339());
|
||||||
|
let system_id: Option<i32> = sub
|
||||||
|
.custom_data
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|d| d.get("system_id"))
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.map(|v| v as i32);
|
||||||
|
|
||||||
|
sqlx::query::<Postgres>(
|
||||||
|
r#"
|
||||||
|
insert into premium_subscriptions (provider, provider_id, email, system_id, status, next_renewal_at)
|
||||||
|
values ('paddle', $1, $2, $3, $4, $5)
|
||||||
|
on conflict (provider, provider_id) do update set
|
||||||
|
status = excluded.status,
|
||||||
|
next_renewal_at = excluded.next_renewal_at
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(sub.id.as_ref())
|
||||||
|
.bind(email)
|
||||||
|
.bind(system_id)
|
||||||
|
.bind(&status)
|
||||||
|
.bind(&next_renewal_at)
|
||||||
|
.execute(&ctx.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// if has a linked system, also update system_config
|
||||||
|
// just in case we get out of order webhooks, never reduce the premium_until
|
||||||
|
// todo: this will obviously break if we refund someone's subscription
|
||||||
|
if let Some(system_id) = system_id {
|
||||||
|
if matches!(sub.status, SubscriptionStatus::Active) {
|
||||||
|
if let Some(next_billed_at) = sub.next_billed_at {
|
||||||
|
let premium_until = next_billed_at.naive_utc();
|
||||||
|
sqlx::query::<Postgres>(
|
||||||
|
r#"
|
||||||
|
update system_config set
|
||||||
|
premium_until = greatest(system_config.premium_until, $2)
|
||||||
|
where system = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(system_id)
|
||||||
|
.bind(premium_until)
|
||||||
|
.execute(&ctx.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"updated premium_until for system {} to {}",
|
||||||
|
system_id, premium_until
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api_endpoint]
|
||||||
|
pub async fn webhook(State(ctx): State<ApiContext>, headers: HeaderMap, body: String) -> Response {
|
||||||
|
let Some(signature) = headers
|
||||||
|
.get("paddle-signature")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
else {
|
||||||
|
return Ok(StatusCode::BAD_REQUEST.into_response());
|
||||||
|
};
|
||||||
|
|
||||||
|
match match Paddle::unmarshal(
|
||||||
|
body,
|
||||||
|
&libpk::config.premium().paddle_webhook_secret,
|
||||||
|
signature,
|
||||||
|
MaximumVariance::default(),
|
||||||
|
) {
|
||||||
|
Ok(event) => event,
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err, "failed to unmarshal paddle data");
|
||||||
|
return Ok(StatusCode::BAD_REQUEST.into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.data
|
||||||
|
{
|
||||||
|
EventData::SubscriptionCreated(sub)
|
||||||
|
| EventData::SubscriptionActivated(sub)
|
||||||
|
| EventData::SubscriptionUpdated(sub) => {
|
||||||
|
match sub.status {
|
||||||
|
SubscriptionStatus::Trialing => {
|
||||||
|
error!(
|
||||||
|
"got status trialing for subscription {}, this should never happen",
|
||||||
|
sub.id
|
||||||
|
);
|
||||||
|
return Ok("".into_response());
|
||||||
|
}
|
||||||
|
SubscriptionStatus::Active
|
||||||
|
| SubscriptionStatus::Canceled
|
||||||
|
| SubscriptionStatus::PastDue
|
||||||
|
| SubscriptionStatus::Paused => {}
|
||||||
|
unk => {
|
||||||
|
error!("got unknown status {unk:?} for subscription {}", sub.id);
|
||||||
|
return Ok("".into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let email = match fetch_customer(sub.customer_id.as_ref()).await {
|
||||||
|
Ok(cus) => cus.email,
|
||||||
|
Err(err) => {
|
||||||
|
fail!(
|
||||||
|
?err,
|
||||||
|
"failed to fetch customer email for subscription {}",
|
||||||
|
sub.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = save_subscription(&ctx, &sub, &email).await {
|
||||||
|
fail!(?err, "failed to save subscription {}", sub.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("saved subscription {} with status {:?}", sub.id, sub.status);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok("".into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel_subscription(subscription_id: &str) -> anyhow::Result<Subscription> {
|
||||||
|
let result = PADDLE_CLIENT
|
||||||
|
.subscription_cancel(subscription_id)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(result.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api_endpoint]
|
||||||
|
pub async fn cancel(
|
||||||
|
State(ctx): State<ApiContext>,
|
||||||
|
axum::Extension(session): axum::Extension<crate::auth::AuthState>,
|
||||||
|
axum::Form(form): axum::Form<CancelForm>,
|
||||||
|
) -> Response {
|
||||||
|
if form.csrf_token != session.csrf_token {
|
||||||
|
return Ok((StatusCode::FORBIDDEN, "invalid csrf token").into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let db_sub = get_subscription(&ctx, &form.subscription_id, &session.email)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(?e, "failed to fetch subscription from db");
|
||||||
|
crate::error::GENERIC_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if db_sub.is_none() {
|
||||||
|
return Ok((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"subscription not found or not owned by you",
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
match cancel_subscription(&form.subscription_id).await {
|
||||||
|
Ok(sub) => {
|
||||||
|
info!("cancelled subscription {} for {}", sub.id, session.email);
|
||||||
|
Ok(axum::response::Redirect::to("/").into_response())
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
fail!(
|
||||||
|
?err,
|
||||||
|
"failed to cancel subscription {}",
|
||||||
|
form.subscription_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct CancelForm {
|
||||||
|
pub csrf_token: String,
|
||||||
|
pub subscription_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct CancelQuery {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel_page(
|
||||||
|
State(ctx): State<ApiContext>,
|
||||||
|
axum::Extension(session): axum::Extension<crate::auth::AuthState>,
|
||||||
|
axum::extract::Query(query): axum::extract::Query<CancelQuery>,
|
||||||
|
) -> Response {
|
||||||
|
let subscriptions = match fetch_subscriptions_for_email(&ctx, &session.email).await {
|
||||||
|
Ok(subs) => subs,
|
||||||
|
Err(e) => {
|
||||||
|
error!(?e, "failed to fetch subscriptions");
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"failed to fetch subscriptions",
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let subscription = subscriptions
|
||||||
|
.into_iter()
|
||||||
|
.find(|s| s.subscription_id() == query.id);
|
||||||
|
|
||||||
|
let Some(subscription) = subscription else {
|
||||||
|
return (
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"subscription not found or not owned by you",
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
};
|
||||||
|
|
||||||
|
axum::response::Html(
|
||||||
|
crate::web::Cancel {
|
||||||
|
csrf_token: session.csrf_token,
|
||||||
|
subscription,
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
67
crates/premium/src/system.rs
Normal file
67
crates/premium/src/system.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
use axum::{
|
||||||
|
Extension, Json,
|
||||||
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::auth::AuthState;
|
||||||
|
use api::ApiContext;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(crate) struct ValidateTokenRequest {
|
||||||
|
csrf_token: String,
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ValidateTokenResponse {
|
||||||
|
system_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ValidateTokenError {
|
||||||
|
error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn validate_token(
|
||||||
|
State(ctx): State<ApiContext>,
|
||||||
|
Extension(session): Extension<AuthState>,
|
||||||
|
Json(body): Json<ValidateTokenRequest>,
|
||||||
|
) -> Response {
|
||||||
|
if body.csrf_token != session.csrf_token {
|
||||||
|
return (
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
Json(ValidateTokenError {
|
||||||
|
error: "Invalid CSRF token.".to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let system_id = match libpk::db::repository::legacy_token_auth(&ctx.db, &body.token).await {
|
||||||
|
Ok(Some(id)) => id,
|
||||||
|
Ok(None) => {
|
||||||
|
return (
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(ValidateTokenError {
|
||||||
|
error: "Invalid system token.".to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(?err, "failed to validate system token");
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(ValidateTokenError {
|
||||||
|
error: "Failed to validate token.".to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Json(ValidateTokenResponse { system_id }).into_response()
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
use crate::auth::AuthState;
|
use crate::auth::AuthState;
|
||||||
|
use crate::paddle::SubscriptionInfo;
|
||||||
|
|
||||||
macro_rules! render {
|
macro_rules! render {
|
||||||
($stuff:expr) => {{
|
($stuff:expr) => {{
|
||||||
|
|
@ -18,16 +19,35 @@ pub(crate) use render;
|
||||||
|
|
||||||
pub fn message(message: String, session: Option<AuthState>) -> Index {
|
pub fn message(message: String, session: Option<AuthState>) -> Index {
|
||||||
Index {
|
Index {
|
||||||
session: session,
|
base_url: libpk::config.premium().base_url.clone(),
|
||||||
|
session,
|
||||||
show_login_form: false,
|
show_login_form: false,
|
||||||
message: Some(message)
|
message: Some(message),
|
||||||
|
subscriptions: vec![],
|
||||||
|
paddle: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
pub struct Index {
|
pub struct Index {
|
||||||
|
pub base_url: String,
|
||||||
pub session: Option<AuthState>,
|
pub session: Option<AuthState>,
|
||||||
pub show_login_form: bool,
|
pub show_login_form: bool,
|
||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
|
pub subscriptions: Vec<SubscriptionInfo>,
|
||||||
|
pub paddle: Option<PaddleData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PaddleData {
|
||||||
|
pub client_token: String,
|
||||||
|
pub price_id: String,
|
||||||
|
pub environment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "cancel.html")]
|
||||||
|
pub struct Cancel {
|
||||||
|
pub csrf_token: String,
|
||||||
|
pub subscription: crate::paddle::SubscriptionInfo,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
crates/premium/templates/cancel.html
Normal file
29
crates/premium/templates/cancel.html
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<title>Cancel Subscription - PluralKit Premium</title>
|
||||||
|
<link rel="stylesheet" href="/static/stylesheet.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>PluralKit Premium</h2>
|
||||||
|
|
||||||
|
{% if subscription.is_cancellable() %}
|
||||||
|
<h3>Cancel Subscription</h3>
|
||||||
|
|
||||||
|
<p>Are you sure you want to cancel subscription <strong>{{ subscription.subscription_id() }}</strong>?</p>
|
||||||
|
<p>Your subscription will remain active until the end of the current billing period.</p>
|
||||||
|
|
||||||
|
<form action="/cancel" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||||
|
<input type="hidden" name="subscription_id" value="{{ subscription.subscription_id() }}" />
|
||||||
|
<button type="submit">Yes, cancel subscription</button>
|
||||||
|
<a href="/"><button type="button">No, go back</button></a>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p>This subscription (<strong>{{ subscription.subscription_id() }}</strong>) has already been canceled and will end on <strong>{{ subscription.next_renewal() }}</strong>.</p>
|
||||||
|
|
||||||
|
<a href="/"><button type="button">Go back</button></a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<br/><br/>
|
||||||
|
<p>for assistance please email us at <a href="mailto:billing@pluralkit.me">billing@pluralkit.me</a></p>
|
||||||
|
</body>
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<head>
|
<head>
|
||||||
<title>PluralKit Premium</title>
|
<title>PluralKit Premium</title>
|
||||||
<link rel="stylesheet" href="/static/stylesheet.css" />
|
<link rel="stylesheet" href="/static/stylesheet.css" />
|
||||||
|
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>PluralKit Premium</h2>
|
<h2>PluralKit Premium</h2>
|
||||||
|
|
@ -9,9 +10,126 @@
|
||||||
{% if let Some(session) = session %}
|
{% if let Some(session) = session %}
|
||||||
<form action="/logout" method="post">
|
<form action="/logout" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="{{ session.csrf_token }}" />
|
<input type="hidden" name="csrf_token" value="{{ session.csrf_token }}" />
|
||||||
<p>logged in as <strong>{{ session.email }}.</strong></p>
|
<p>
|
||||||
|
logged in as <strong>{{ session.email }}.</strong>
|
||||||
<button type="submit">log out</button>
|
<button type="submit">log out</button>
|
||||||
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
{% if subscriptions.is_empty() %}
|
||||||
|
<p>You are not currently subscribed to PluralKit Premium.</p>
|
||||||
|
<p>Enter your system token to subscribe. yes this will be fixed before release</p>
|
||||||
|
<div>
|
||||||
|
<input type="text" id="system-token" placeholder="token" required />
|
||||||
|
<button id="buy-button">Subscribe to PluralKit Premium</button>
|
||||||
|
</div>
|
||||||
|
<p id="token-error" style="color: red; display: none;"></p>
|
||||||
|
<p id="system-info" style="color: green; display: none;"></p>
|
||||||
|
{% else %}
|
||||||
|
You are currently subscribed to PluralKit Premium. Thanks for the support!
|
||||||
|
<br/>
|
||||||
|
{% for sub in &subscriptions %}
|
||||||
|
<p>
|
||||||
|
<strong>Subscription ID:</strong> {{ sub.subscription_id() }}<br/>
|
||||||
|
<strong>Status:</strong> {{ sub.status() }}<br/>
|
||||||
|
<strong>Next Renewal:</strong> {{ sub.next_renewal() }}<br/>
|
||||||
|
<strong>Linked System:</strong> {{ sub.system_id_display()|safe }}<br/>
|
||||||
|
{% if sub.is_cancellable() %}
|
||||||
|
<a href="/cancel?id={{ sub.subscription_id() }}">Cancel</a><br/>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if let Some(paddle) = paddle %}
|
||||||
|
<script>
|
||||||
|
Paddle.Environment.set("{{ paddle.environment }}");
|
||||||
|
Paddle.Initialize({
|
||||||
|
token: "{{ paddle.client_token }}",
|
||||||
|
eventCallback: function(event) {
|
||||||
|
if (event.name === "checkout.completed") {
|
||||||
|
// webhook request sometimes takes a while, artificially delay here
|
||||||
|
document.body.innerHTML = "<h2>PluralKit Premium</h2><p>Processing your subscription, please wait...</p>";
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.href = "{{ base_url }}";
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const buyButton = document.getElementById("buy-button");
|
||||||
|
if (buyButton) {
|
||||||
|
buyButton.addEventListener("click", async function() {
|
||||||
|
const tokenInput = document.getElementById("system-token");
|
||||||
|
const errorEl = document.getElementById("token-error");
|
||||||
|
const infoEl = document.getElementById("system-info");
|
||||||
|
|
||||||
|
if (!tokenInput || !tokenInput.value.trim()) {
|
||||||
|
errorEl.textContent = "Please enter your system token.";
|
||||||
|
errorEl.style.display = "block";
|
||||||
|
infoEl.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the token
|
||||||
|
buyButton.disabled = true;
|
||||||
|
buyButton.textContent = "Validating...";
|
||||||
|
errorEl.style.display = "none";
|
||||||
|
infoEl.style.display = "none";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/validate-token", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
csrf_token: "{{ session.csrf_token }}",
|
||||||
|
token: tokenInput.value.trim()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
errorEl.textContent = data.error || "Invalid token.";
|
||||||
|
errorEl.style.display = "block";
|
||||||
|
buyButton.disabled = false;
|
||||||
|
buyButton.textContent = "Subscribe to PluralKit Premium";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token is valid, open Paddle checkout
|
||||||
|
Paddle.Checkout.open({
|
||||||
|
settings: {
|
||||||
|
allowLogout: false,
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{ priceId: "{{ paddle.price_id }}", quantity: 1 }
|
||||||
|
],
|
||||||
|
customer: {
|
||||||
|
email: "{{ session.email }}"
|
||||||
|
},
|
||||||
|
customData: {
|
||||||
|
email: "{{ session.email }}",
|
||||||
|
system_id: data.system_id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
buyButton.disabled = false;
|
||||||
|
buyButton.textContent = "Subscribe to PluralKit Premium";
|
||||||
|
} catch (err) {
|
||||||
|
errorEl.textContent = "Failed to validate token. Please try again.";
|
||||||
|
errorEl.style.display = "block";
|
||||||
|
buyButton.disabled = false;
|
||||||
|
buyButton.textContent = "Subscribe to PluralKit Premium";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
error initializing paddle client
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_login_form %}
|
{% if show_login_form %}
|
||||||
|
|
@ -26,4 +144,7 @@
|
||||||
{% if let Some(msg) = message %}
|
{% if let Some(msg) = message %}
|
||||||
<div>{{ msg }}</div>
|
<div>{{ msg }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<br/><br/>
|
||||||
|
<p>for assistance please email us at <a href="mailto:billing@pluralkit.me">billing@pluralkit.me</a></p>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue