feat: generate invoices
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
closes #29
This commit is contained in:
72
Cargo.lock
generated
72
Cargo.lock
generated
@ -1000,7 +1000,7 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1014,7 +1014,7 @@ dependencies = [
|
|||||||
"anstyle",
|
"anstyle",
|
||||||
"env_filter",
|
"env_filter",
|
||||||
"jiff",
|
"jiff",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1266,7 +1266,7 @@ checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"windows 0.48.0",
|
"windows 0.48.0",
|
||||||
]
|
]
|
||||||
@ -1700,7 +1700,7 @@ dependencies = [
|
|||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"iana-time-zone-haiku",
|
"iana-time-zone-haiku",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.61.0",
|
"windows-core 0.61.0",
|
||||||
]
|
]
|
||||||
@ -1969,7 +1969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260"
|
checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jiff-static",
|
"jiff-static",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"portable-atomic-util",
|
"portable-atomic-util",
|
||||||
"serde",
|
"serde",
|
||||||
@ -2142,7 +2142,8 @@ dependencies = [
|
|||||||
"lnurl-rs",
|
"lnurl-rs",
|
||||||
"lnvps_common",
|
"lnvps_common",
|
||||||
"lnvps_db",
|
"lnvps_db",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
|
"mustache",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"nostr",
|
"nostr",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
@ -2194,7 +2195,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"lnvps_common",
|
"lnvps_common",
|
||||||
"lnvps_db",
|
"lnvps_db",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"rocket",
|
"rocket",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -2211,6 +2212,15 @@ dependencies = [
|
|||||||
"scopeguard",
|
"scopeguard",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
|
||||||
|
dependencies = [
|
||||||
|
"log 0.4.27",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.27"
|
version = "0.4.27"
|
||||||
@ -2320,6 +2330,16 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mustache"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51956ef1c5d20a1384524d91e616fb44dfc7d8f249bf696d49c97dd3289ecab5"
|
||||||
|
dependencies = [
|
||||||
|
"log 0.3.9",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
@ -2327,7 +2347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"openssl",
|
"openssl",
|
||||||
"openssl-probe",
|
"openssl-probe",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
@ -2513,7 +2533,7 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a64853d7ab065474e87696f7601cee817d200e86c42e04004e005cb3e20c3c5"
|
checksum = "9a64853d7ab065474e87696f7601cee817d200e86c42e04004e005cb3e20c3c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -2938,7 +2958,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"heck",
|
"heck",
|
||||||
"itertools",
|
"itertools",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"multimap",
|
"multimap",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
@ -3166,7 +3186,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"mime",
|
"mime",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -3228,7 +3248,7 @@ dependencies = [
|
|||||||
"figment",
|
"figment",
|
||||||
"futures",
|
"futures",
|
||||||
"indexmap 2.8.0",
|
"indexmap 2.8.0",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"memchr",
|
"memchr",
|
||||||
"multer",
|
"multer",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
@ -3280,7 +3300,7 @@ dependencies = [
|
|||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"hyper 0.14.32",
|
"hyper 0.14.32",
|
||||||
"indexmap 2.8.0",
|
"indexmap 2.8.0",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"memchr",
|
"memchr",
|
||||||
"pear",
|
"pear",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@ -3301,7 +3321,7 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "074297bec35db2fc7ebb6ade6a955b5566de66f83d9af5b5602a350a71bdef43"
|
checksum = "074297bec35db2fc7ebb6ade6a955b5566de66f83d9af5b5602a350a71bdef43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"okapi",
|
"okapi",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_okapi_codegen",
|
"rocket_okapi_codegen",
|
||||||
@ -3402,7 +3422,7 @@ version = "0.21.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-webpki 0.101.7",
|
"rustls-webpki 0.101.7",
|
||||||
"sct",
|
"sct",
|
||||||
@ -3814,7 +3834,7 @@ dependencies = [
|
|||||||
"hashbrown 0.15.2",
|
"hashbrown 0.15.2",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
"indexmap 2.8.0",
|
"indexmap 2.8.0",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@ -3893,7 +3913,7 @@ dependencies = [
|
|||||||
"hkdf",
|
"hkdf",
|
||||||
"hmac",
|
"hmac",
|
||||||
"itoa",
|
"itoa",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -3933,7 +3953,7 @@ dependencies = [
|
|||||||
"hmac",
|
"hmac",
|
||||||
"home",
|
"home",
|
||||||
"itoa",
|
"itoa",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -3964,7 +3984,7 @@ dependencies = [
|
|||||||
"futures-intrusive",
|
"futures-intrusive",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@ -4388,7 +4408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
@ -4402,7 +4422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"rustls 0.23.25",
|
"rustls 0.23.25",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -4554,7 +4574,7 @@ version = "0.1.41"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
@ -4587,7 +4607,7 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
@ -4633,7 +4653,7 @@ dependencies = [
|
|||||||
"data-encoding",
|
"data-encoding",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"sha1",
|
"sha1",
|
||||||
@ -4652,7 +4672,7 @@ dependencies = [
|
|||||||
"data-encoding",
|
"data-encoding",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"httparse",
|
"httparse",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"rustls 0.23.25",
|
"rustls 0.23.25",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@ -4885,7 +4905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log 0.4.27",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.100",
|
"syn 2.0.100",
|
||||||
|
@ -57,6 +57,7 @@ lettre = { version = "0.11.10", features = ["tokio1-native-tls"] }
|
|||||||
ws = { package = "rocket_ws", version = "0.1.1" }
|
ws = { package = "rocket_ws", version = "0.1.1" }
|
||||||
native-tls = "0.2.12"
|
native-tls = "0.2.12"
|
||||||
lnurl-rs = { version = "0.9.0", default-features = false }
|
lnurl-rs = { version = "0.9.0", default-features = false }
|
||||||
|
mustache = "0.9.0"
|
||||||
|
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
isocountry = "0.3.2"
|
isocountry = "0.3.2"
|
||||||
|
159
lnvps_api/invoice.html
Normal file
159
lnvps_api/invoice.html
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{payment.id}}</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: "Source Code Pro", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen {
|
||||||
|
.page {
|
||||||
|
margin-left: 4rem;
|
||||||
|
margin-right: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 0.4em 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
text-align: end;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.5em 0.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<div class="header">
|
||||||
|
LNVPS
|
||||||
|
<img height="48" width="48" src="https://lnvps.net/logo.jpg" alt="logo"/>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<h2>Invoice</h2>
|
||||||
|
<div class="flex-col">
|
||||||
|
<div>
|
||||||
|
<b>ID:</b>
|
||||||
|
{{payment.id}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Date:</b>
|
||||||
|
{{payment.created}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Status:</b>
|
||||||
|
{{#payment.is_paid}}Paid{{/payment.is_paid}}
|
||||||
|
{{^payment.is_paid}}Unpaid{{/payment.is_paid}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>Nostr Pubkey:</b>
|
||||||
|
{{npub}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="billing">
|
||||||
|
<div class="flex-col">
|
||||||
|
<h2>Bill To:</h2>
|
||||||
|
<div>{{user.name}}</div>
|
||||||
|
<div>{{user.address_1}}</div>
|
||||||
|
<div>{{user.address_2}}</div>
|
||||||
|
<div>{{user.city}}</div>
|
||||||
|
<div>{{user.state}}</div>
|
||||||
|
<div>{{user.postcode}}</div>
|
||||||
|
<div>{{user.country}}</div>
|
||||||
|
<div>{{user.tax_id}}</div>
|
||||||
|
</div>
|
||||||
|
{{#company}}
|
||||||
|
<div class="flex-col">
|
||||||
|
<h2> </h2>
|
||||||
|
<div>{{company.name}}</div>
|
||||||
|
<div>{{company.address_1}}</div>
|
||||||
|
<div>{{company.address_2}}</div>
|
||||||
|
<div>{{company.city}}</div>
|
||||||
|
<div>{{company.state}}</div>
|
||||||
|
<div>{{company.postcode}}</div>
|
||||||
|
<div>{{company.country}}</div>
|
||||||
|
<div>{{company.tax_id}}</div>
|
||||||
|
</div>
|
||||||
|
{{/company}}
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<h2>Details:</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Currency</th>
|
||||||
|
<th>Gross</th>
|
||||||
|
<th>Taxes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
VM Renewal #{{vm.id}}
|
||||||
|
- {{vm.template.name}}
|
||||||
|
- {{vm.image.distribution}} {{vm.image.version}}
|
||||||
|
- {{payment.time}} seconds
|
||||||
|
</td>
|
||||||
|
<td>{{payment.currency}}</td>
|
||||||
|
<td>{{payment.amount}}</td>
|
||||||
|
<td>{{payment.tax}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="total">
|
||||||
|
Total: {{total}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br/>
|
||||||
|
<b>
|
||||||
|
All BTC amounts are in milli-satoshis and all fiat amounts are in cents.
|
||||||
|
</b>
|
||||||
|
<hr/>
|
||||||
|
<small>
|
||||||
|
(c) {{year}} LNVPS.net - Generated at {{current_date}}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -425,6 +425,24 @@ pub struct AccountPatchRequest {
|
|||||||
pub tax_id: Option<String>,
|
pub tax_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<lnvps_db::User> for AccountPatchRequest {
|
||||||
|
fn from(user: lnvps_db::User) -> Self {
|
||||||
|
AccountPatchRequest {
|
||||||
|
email: user.email,
|
||||||
|
contact_nip17: user.contact_nip17,
|
||||||
|
contact_email: user.contact_email,
|
||||||
|
country_code: user.country_code,
|
||||||
|
name: user.billing_name,
|
||||||
|
address_1: user.billing_address_1,
|
||||||
|
address_2: user.billing_address_2,
|
||||||
|
state: user.billing_state,
|
||||||
|
city: user.billing_city,
|
||||||
|
postcode: user.billing_postcode,
|
||||||
|
tax_id: user.billing_tax_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct CreateVmRequest {
|
pub struct CreateVmRequest {
|
||||||
pub template_id: u64,
|
pub template_id: u64,
|
||||||
@ -574,3 +592,45 @@ impl From<PaymentMethod> for ApiPaymentMethod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct ApiCompany {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub email: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub country_code: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub address_1: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub address_2: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub state: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub city: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub postcode: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tax_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub phone: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lnvps_db::Company> for ApiCompany {
|
||||||
|
fn from(value: lnvps_db::Company) -> Self {
|
||||||
|
Self {
|
||||||
|
email: value.email,
|
||||||
|
country_code: value.country_code,
|
||||||
|
name: value.name,
|
||||||
|
id: value.id,
|
||||||
|
address_1: value.address_1,
|
||||||
|
address_2: value.address_2,
|
||||||
|
state: value.state,
|
||||||
|
city: value.city,
|
||||||
|
postcode: value.postcode,
|
||||||
|
tax_id: value.tax_id,
|
||||||
|
phone: value.phone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::api::model::{
|
use crate::api::model::{
|
||||||
AccountPatchRequest, ApiCustomTemplateParams, ApiCustomVmOrder, ApiCustomVmRequest,
|
AccountPatchRequest, ApiCompany, ApiCustomTemplateParams, ApiCustomVmOrder, ApiCustomVmRequest,
|
||||||
ApiPaymentInfo, ApiPaymentMethod, ApiPrice, ApiTemplatesResponse, ApiUserSshKey,
|
ApiPaymentInfo, ApiPaymentMethod, ApiPrice, ApiTemplatesResponse, ApiUserSshKey,
|
||||||
ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus, ApiVmTemplate, CreateSshKey,
|
ApiVmIpAssignment, ApiVmOsImage, ApiVmPayment, ApiVmStatus, ApiVmTemplate, CreateSshKey,
|
||||||
CreateVmRequest, VMPatchRequest,
|
CreateVmRequest, VMPatchRequest,
|
||||||
@ -12,6 +12,7 @@ use crate::settings::Settings;
|
|||||||
use crate::status::{VmState, VmStateCache};
|
use crate::status::{VmState, VmStateCache};
|
||||||
use crate::worker::WorkJob;
|
use crate::worker::WorkJob;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
use chrono::{DateTime, Datelike, Utc};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
use isocountry::CountryCode;
|
use isocountry::CountryCode;
|
||||||
@ -22,7 +23,8 @@ use lnvps_db::{
|
|||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use nostr::util::hex;
|
use nostr::util::hex;
|
||||||
use nostr::Url;
|
use nostr::{ToBech32, Url};
|
||||||
|
use rocket::http::ContentType;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::{get, patch, post, routes, Responder, Route, State};
|
use rocket::{get, patch, post, routes, Responder, Route, State};
|
||||||
use rocket_okapi::gen::OpenApiGenerator;
|
use rocket_okapi::gen::OpenApiGenerator;
|
||||||
@ -34,6 +36,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use ssh_key::PublicKey;
|
use ssh_key::PublicKey;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::io::{BufWriter, Cursor};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc::{Sender, UnboundedSender};
|
use tokio::sync::mpsc::{Sender, UnboundedSender};
|
||||||
@ -71,7 +74,8 @@ pub fn routes() -> Vec<Route> {
|
|||||||
routes.append(&mut routes![
|
routes.append(&mut routes![
|
||||||
v1_terminal_proxy,
|
v1_terminal_proxy,
|
||||||
v1_lnurlp,
|
v1_lnurlp,
|
||||||
v1_renew_vm_lnurlp
|
v1_renew_vm_lnurlp,
|
||||||
|
v1_get_payment_invoice
|
||||||
]);
|
]);
|
||||||
|
|
||||||
routes
|
routes
|
||||||
@ -156,19 +160,7 @@ async fn v1_get_account(
|
|||||||
let uid = db.upsert_user(&pubkey).await?;
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
let user = db.get_user(uid).await?;
|
let user = db.get_user(uid).await?;
|
||||||
|
|
||||||
ApiData::ok(AccountPatchRequest {
|
ApiData::ok(user.into())
|
||||||
email: user.email,
|
|
||||||
contact_nip17: user.contact_nip17,
|
|
||||||
contact_email: user.contact_email,
|
|
||||||
country_code: user.country_code,
|
|
||||||
name: user.billing_name,
|
|
||||||
address_1: user.billing_address_1,
|
|
||||||
address_2: user.billing_address_2,
|
|
||||||
state: user.billing_state,
|
|
||||||
city: user.billing_city,
|
|
||||||
postcode: user.billing_postcode,
|
|
||||||
tax_id: user.billing_tax_id,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn vm_to_status(
|
async fn vm_to_status(
|
||||||
@ -894,6 +886,99 @@ async fn v1_get_payment(
|
|||||||
ApiData::ok(payment.into())
|
ApiData::ok(payment.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Print payment invoice
|
||||||
|
#[get("/api/v1/payment/<id>/invoice?<auth>")]
|
||||||
|
async fn v1_get_payment_invoice(
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
id: &str,
|
||||||
|
auth: &str,
|
||||||
|
) -> Result<(ContentType, Vec<u8>), &'static str> {
|
||||||
|
let auth = Nip98Auth::from_base64(auth).map_err(|e| "Missing or invalid auth param")?;
|
||||||
|
if auth
|
||||||
|
.check(&format!("/api/v1/payment/{id}/invoice"), "GET")
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return Err("Invalid auth event");
|
||||||
|
}
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await.map_err(|_| "Insert failed")?;
|
||||||
|
let id = if let Ok(i) = hex::decode(id) {
|
||||||
|
i
|
||||||
|
} else {
|
||||||
|
return Err("Invalid payment id");
|
||||||
|
};
|
||||||
|
|
||||||
|
let payment = db
|
||||||
|
.get_vm_payment(&id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Payment not found")?;
|
||||||
|
let vm = db.get_vm(payment.vm_id).await.map_err(|_| "VM not found")?;
|
||||||
|
if vm.user_id != uid {
|
||||||
|
return Err("VM does not belong to you");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !payment.is_paid {
|
||||||
|
return Err("Payment is not paid, can't generate invoice");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct PaymentInfo {
|
||||||
|
year: i32,
|
||||||
|
current_date: DateTime<Utc>,
|
||||||
|
vm: ApiVmStatus,
|
||||||
|
payment: ApiVmPayment,
|
||||||
|
user: AccountPatchRequest,
|
||||||
|
npub: String,
|
||||||
|
total: u64,
|
||||||
|
company: Option<ApiCompany>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let host = db
|
||||||
|
.get_host(vm.host_id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Host not found")?;
|
||||||
|
let region = db
|
||||||
|
.get_host_region(host.region_id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Region not found")?;
|
||||||
|
let company = if let Some(c) = region.company_id {
|
||||||
|
Some(db.get_company(c).await.map_err(|_| "Company not found")?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let user = db.get_user(uid).await.map_err(|_| "User not found")?;
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let template =
|
||||||
|
mustache::compile_path("lnvps_api/invoice.html").map_err(|_| "Invalid template")?;
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let template = mustache::compile_str(include_str!("../../invoice.html"))
|
||||||
|
.map_err(|_| "Invalid template")?;
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
|
let mut html = Cursor::new(Vec::new());
|
||||||
|
template
|
||||||
|
.render(
|
||||||
|
&mut html,
|
||||||
|
&PaymentInfo {
|
||||||
|
year: now.year(),
|
||||||
|
current_date: now,
|
||||||
|
vm: vm_to_status(db, vm, None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Failed to get VM state")?,
|
||||||
|
total: payment.amount + payment.tax,
|
||||||
|
payment: payment.into(),
|
||||||
|
npub: nostr::PublicKey::from_slice(&user.pubkey)
|
||||||
|
.map_err(|_| "Invalid pubkey")?
|
||||||
|
.to_bech32()
|
||||||
|
.unwrap(),
|
||||||
|
user: user.into(),
|
||||||
|
company: company.map(|c| c.into()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|_| "Failed to generate invoice")?;
|
||||||
|
Ok((ContentType::HTML, html.into_inner()))
|
||||||
|
}
|
||||||
|
|
||||||
/// List payment history of a VM
|
/// List payment history of a VM
|
||||||
#[openapi(tag = "VM")]
|
#[openapi(tag = "VM")]
|
||||||
#[get("/api/v1/vm/<id>/payments")]
|
#[get("/api/v1/vm/<id>/payments")]
|
||||||
|
@ -10,12 +10,7 @@ use crate::status::{VmRunningState, VmState};
|
|||||||
use anyhow::{anyhow, bail, ensure, Context};
|
use anyhow::{anyhow, bail, ensure, Context};
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream;
|
use fedimint_tonic_lnd::tonic::codegen::tokio_stream::Stream;
|
||||||
use lnvps_db::{
|
use lnvps_db::{async_trait, AccessPolicy, Company, DiskInterface, DiskType, IpRange, IpRangeAllocationMode, LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, OsDistribution, User, UserSshKey, Vm, VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate};
|
||||||
async_trait, AccessPolicy, DiskInterface, DiskType, IpRange, IpRangeAllocationMode,
|
|
||||||
LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, OsDistribution, User, UserSshKey, Vm,
|
|
||||||
VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate,
|
|
||||||
VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
|
||||||
};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
@ -108,6 +103,7 @@ impl Default for MockDb {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: "Mock".to_string(),
|
name: "Mock".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
company_id: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let mut ip_ranges = HashMap::new();
|
let mut ip_ranges = HashMap::new();
|
||||||
@ -695,6 +691,10 @@ impl LNVpsDb for MockDb {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.context("no access policy")?)
|
.context("no access policy")?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_company(&self, company_id: u64) -> anyhow::Result<Company> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
19
lnvps_db/migrations/20250501162308_company_info.sql
Normal file
19
lnvps_db/migrations/20250501162308_company_info.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
create table company
|
||||||
|
(
|
||||||
|
id integer unsigned not null auto_increment primary key,
|
||||||
|
created timestamp not null default current_timestamp,
|
||||||
|
name varchar(100) not null,
|
||||||
|
email varchar(100) not null,
|
||||||
|
phone varchar(100),
|
||||||
|
address_1 varchar(200),
|
||||||
|
address_2 varchar(200),
|
||||||
|
city varchar(100),
|
||||||
|
state varchar(100),
|
||||||
|
postcode varchar(50),
|
||||||
|
country_code varchar(3),
|
||||||
|
tax_id varchar(50)
|
||||||
|
);
|
||||||
|
alter table vm_host_region
|
||||||
|
add column company_id integer unsigned,
|
||||||
|
add constraint fk_host_region_company foreign key (company_id) references company (id);
|
@ -172,6 +172,9 @@ pub trait LNVpsDb: LNVPSNostrDb + Send + Sync {
|
|||||||
|
|
||||||
/// Get access policy
|
/// Get access policy
|
||||||
async fn get_access_policy(&self, access_policy_id: u64) -> Result<AccessPolicy>;
|
async fn get_access_policy(&self, access_policy_id: u64) -> Result<AccessPolicy>;
|
||||||
|
|
||||||
|
/// Get company
|
||||||
|
async fn get_company(&self, company_id: u64) -> Result<Company>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "nostr-domain")]
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
@ -71,6 +71,7 @@ pub struct VmHostRegion {
|
|||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
pub company_id: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Clone, Debug, Default)]
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
@ -524,3 +525,19 @@ pub struct NostrDomainHandle {
|
|||||||
pub pubkey: Vec<u8>,
|
pub pubkey: Vec<u8>,
|
||||||
pub relays: Option<String>,
|
pub relays: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
|
pub struct Company {
|
||||||
|
pub id: u64,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub name: String,
|
||||||
|
pub address_1: Option<String>,
|
||||||
|
pub address_2: Option<String>,
|
||||||
|
pub city: Option<String>,
|
||||||
|
pub state: Option<String>,
|
||||||
|
pub country_code: Option<String>,
|
||||||
|
pub tax_id: Option<String>,
|
||||||
|
pub postcode: Option<String>,
|
||||||
|
pub phone: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
}
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
use crate::{
|
use crate::{AccessPolicy, Company, IpRange, LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, Router, User, UserSshKey, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate};
|
||||||
AccessPolicy, IpRange, LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, Router, User,
|
|
||||||
UserSshKey, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost,
|
|
||||||
VmHostDisk, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
|
||||||
};
|
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{bail, Error, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{Executor, MySqlPool, Row};
|
use sqlx::{Executor, MySqlPool, Row};
|
||||||
@ -562,6 +558,14 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
.await
|
.await
|
||||||
.map_err(Error::new)
|
.map_err(Error::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_company(&self, company_id: u64) -> Result<Company> {
|
||||||
|
sqlx::query_as("select * from company where id=?")
|
||||||
|
.bind(company_id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "nostr-domain")]
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
Reference in New Issue
Block a user