feat: nostr domain hosting
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -2038,7 +2038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
|
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lnvps"
|
name = "lnvps_api"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -2054,6 +2054,7 @@ dependencies = [
|
|||||||
"ipnetwork",
|
"ipnetwork",
|
||||||
"isocountry",
|
"isocountry",
|
||||||
"lettre",
|
"lettre",
|
||||||
|
"lnvps_common",
|
||||||
"lnvps_db",
|
"lnvps_db",
|
||||||
"log",
|
"log",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
@ -2079,6 +2080,13 @@ dependencies = [
|
|||||||
"virt",
|
"virt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lnvps_common"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"rocket",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lnvps_db"
|
name = "lnvps_db"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -2094,7 +2102,17 @@ dependencies = [
|
|||||||
name = "lnvps_nostr"
|
name = "lnvps_nostr"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"config",
|
||||||
|
"env_logger",
|
||||||
|
"hex",
|
||||||
|
"lnvps_common",
|
||||||
"lnvps_db",
|
"lnvps_db",
|
||||||
|
"log",
|
||||||
|
"rocket",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
12
Cargo.toml
12
Cargo.toml
@ -3,11 +3,17 @@ resolver = "3"
|
|||||||
members = [
|
members = [
|
||||||
"lnvps_db",
|
"lnvps_db",
|
||||||
"lnvps_api",
|
"lnvps_api",
|
||||||
"lnvps_nostr"
|
"lnvps_nostr",
|
||||||
|
"lnvps_common"
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros", "sync", "io-util"] }
|
tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||||
anyhow = "1.0.83"
|
anyhow = "1.0.83"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
env_logger = "0.11.7"
|
env_logger = "0.11.7"
|
||||||
|
serde = { version = "1.0.213", features = ["derive"] }
|
||||||
|
serde_json = "1.0.132"
|
||||||
|
rocket = { version = "0.5.1", features = ["json"] }
|
||||||
|
config = { version = "0.15.8", features = ["yaml"] }
|
||||||
|
hex = "0.4.3"
|
@ -4,9 +4,11 @@ FROM $IMAGE AS build
|
|||||||
WORKDIR /app/src
|
WORKDIR /app/src
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN apt update && apt -y install protobuf-compiler libvirt-dev
|
RUN apt update && apt -y install protobuf-compiler libvirt-dev
|
||||||
RUN cargo test && cargo install --path lnvps_api --root /app/build
|
RUN cargo test \
|
||||||
|
&& cargo install --root /app/build --path lnvps_api \
|
||||||
|
&& cargo install --root /app/build --path lnvps_nostr
|
||||||
|
|
||||||
FROM $IMAGE AS runner
|
FROM $IMAGE AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/build .
|
COPY --from=build /app/build .
|
||||||
ENTRYPOINT ["./bin/api"]
|
ENTRYPOINT ["./bin/lnvps_api"]
|
@ -1,25 +1,30 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lnvps"
|
name = "lnvps_api"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "api"
|
name = "lnvps_api"
|
||||||
|
path = "src/bin/api.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
"mikrotik",
|
"mikrotik",
|
||||||
"nostr-dm",
|
"nostr-dm",
|
||||||
"nostr-dvm",
|
"nostr-dvm",
|
||||||
|
"nostr-domain",
|
||||||
"proxmox",
|
"proxmox",
|
||||||
"lnd",
|
"lnd",
|
||||||
"cloudflare",
|
"cloudflare",
|
||||||
"revolut",
|
"revolut",
|
||||||
"bitvora"
|
"bitvora",
|
||||||
|
"tokio/sync",
|
||||||
|
"tokio/io-util"
|
||||||
]
|
]
|
||||||
mikrotik = ["dep:reqwest"]
|
mikrotik = ["dep:reqwest"]
|
||||||
nostr-dm = ["dep:nostr-sdk"]
|
nostr-dm = ["dep:nostr-sdk"]
|
||||||
nostr-dvm = ["dep:nostr-sdk"]
|
nostr-dvm = ["dep:nostr-sdk"]
|
||||||
|
nostr-domain = ["lnvps_db/nostr-domain"]
|
||||||
proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"]
|
proxmox = ["dep:reqwest", "dep:ssh2", "dep:tokio-tungstenite"]
|
||||||
libvirt = ["dep:virt", "dep:uuid", "dep:quick-xml"]
|
libvirt = ["dep:virt", "dep:uuid", "dep:quick-xml"]
|
||||||
lnd = ["dep:fedimint-tonic-lnd"]
|
lnd = ["dep:fedimint-tonic-lnd"]
|
||||||
@ -29,15 +34,16 @@ revolut = ["dep:reqwest", "dep:sha2", "dep:hmac"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lnvps_db = { path = "../lnvps_db" }
|
lnvps_db = { path = "../lnvps_db" }
|
||||||
|
lnvps_common = { path = "../lnvps_common" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
tokio = { version = "1.37.0", features = ["rt", "rt-multi-thread", "macros", "sync", "io-util"] }
|
config.workspace = true
|
||||||
config = { version = "0.15.8", features = ["yaml"] }
|
serde.workspace = true
|
||||||
serde = { version = "1.0.213", features = ["derive"] }
|
serde_json.workspace = true
|
||||||
serde_json = "1.0.132"
|
rocket.workspace = true
|
||||||
rocket = { version = "0.5.1", features = ["json"] }
|
hex.workspace = true
|
||||||
rocket_okapi = { version = "0.9.0", features = ["swagger"] }
|
rocket_okapi = { version = "0.9.0", features = ["swagger"] }
|
||||||
schemars = { version = "0.8.22", features = ["chrono"] }
|
schemars = { version = "0.8.22", features = ["chrono"] }
|
||||||
chrono = { version = "0.4.38", features = ["serde"] }
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
@ -50,7 +56,7 @@ ssh-key = "0.6.7"
|
|||||||
lettre = { version = "0.11.10", features = ["tokio1-native-tls"] }
|
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"
|
||||||
hex = "0.4.3"
|
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
isocountry = "0.3.2"
|
isocountry = "0.3.2"
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
|
|
||||||
mod model;
|
mod model;
|
||||||
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
mod nostr_domain;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod webhook;
|
mod webhook;
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ use std::collections::HashMap;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiVmStatus {
|
pub struct ApiVmStatus {
|
||||||
/// Unique VM ID (Same in proxmox)
|
/// Unique VM ID (Same in proxmox)
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
@ -37,7 +37,7 @@ pub struct ApiVmStatus {
|
|||||||
pub status: VmState,
|
pub status: VmState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiUserSshKey {
|
pub struct ApiUserSshKey {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -54,7 +54,7 @@ impl From<lnvps_db::UserSshKey> for ApiUserSshKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiVmIpAssignment {
|
pub struct ApiVmIpAssignment {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub ip: String,
|
pub ip: String,
|
||||||
@ -133,7 +133,7 @@ impl From<DiskInterface> for lnvps_db::DiskInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiTemplatesResponse {
|
pub struct ApiTemplatesResponse {
|
||||||
pub templates: Vec<ApiVmTemplate>,
|
pub templates: Vec<ApiVmTemplate>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@ -157,7 +157,7 @@ impl ApiTemplatesResponse {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, JsonSchema)]
|
||||||
pub struct ApiCustomTemplateParams {
|
pub struct ApiCustomTemplateParams {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -196,8 +196,7 @@ impl ApiCustomTemplateParams {
|
|||||||
.filter_map(|d| {
|
.filter_map(|d| {
|
||||||
Some(ApiCustomTemplateDiskParam {
|
Some(ApiCustomTemplateDiskParam {
|
||||||
min_disk: GB * 5,
|
min_disk: GB * 5,
|
||||||
max_disk: *max_disk
|
max_disk: *max_disk.get(&(d.kind.into(), d.interface.into()))?,
|
||||||
.get(&(d.kind.into(), d.interface.into()))?,
|
|
||||||
disk_type: d.kind.into(),
|
disk_type: d.kind.into(),
|
||||||
disk_interface: d.interface.into(),
|
disk_interface: d.interface.into(),
|
||||||
})
|
})
|
||||||
|
202
lnvps_api/src/api/nostr_domain.rs
Normal file
202
lnvps_api/src/api/nostr_domain.rs
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
use crate::api::routes::{ApiData, ApiResult};
|
||||||
|
use crate::nip98::Nip98Auth;
|
||||||
|
use crate::settings::Settings;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use lnvps_db::{LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
|
use rocket::{delete, get, post, routes, Route, State};
|
||||||
|
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||||
|
use rocket_okapi::settings::OpenApiSettings;
|
||||||
|
use rocket_okapi::{openapi, openapi_get_routes, openapi_routes, JsonSchema};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![
|
||||||
|
v1_nostr_domains,
|
||||||
|
v1_create_nostr_domain,
|
||||||
|
v1_list_nostr_domain_handles,
|
||||||
|
v1_create_nostr_domain_handle,
|
||||||
|
v1_delete_nostr_domain_handle
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[get("/api/v1/nostr/domain")]
|
||||||
|
async fn v1_nostr_domains(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
settings: &State<Settings>,
|
||||||
|
) -> ApiResult<ApiDomainsResponse> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let domains = db.list_domains(uid).await?;
|
||||||
|
ApiData::ok(ApiDomainsResponse {
|
||||||
|
domains: domains.into_iter().map(|d| d.into()).collect(),
|
||||||
|
cname: settings.nostr_address_host.clone().unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[post("/api/v1/nostr/domain", format = "json", data = "<data>")]
|
||||||
|
async fn v1_create_nostr_domain(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
data: Json<NameRequest>,
|
||||||
|
) -> ApiResult<ApiNostrDomain> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let mut dom = NostrDomain {
|
||||||
|
owner_id: uid,
|
||||||
|
name: data.name.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let dom_id = db.insert_domain(&dom).await?;
|
||||||
|
dom.id = dom_id;
|
||||||
|
|
||||||
|
ApiData::ok(dom.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[get("/api/v1/nostr/domain/<dom>/handle")]
|
||||||
|
async fn v1_list_nostr_domain_handles(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
dom: u64,
|
||||||
|
) -> ApiResult<Vec<ApiNostrDomainHandle>> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let domain = db.get_domain(dom).await?;
|
||||||
|
if domain.owner_id != uid {
|
||||||
|
return ApiData::err("Access denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
let handles = db.list_handles(domain.id).await?;
|
||||||
|
ApiData::ok(handles.into_iter().map(|h| h.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[post("/api/v1/nostr/domain/<dom>/handle", format = "json", data = "<data>")]
|
||||||
|
async fn v1_create_nostr_domain_handle(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
dom: u64,
|
||||||
|
data: Json<HandleRequest>,
|
||||||
|
) -> ApiResult<ApiNostrDomainHandle> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let domain = db.get_domain(dom).await?;
|
||||||
|
if domain.owner_id != uid {
|
||||||
|
return ApiData::err("Access denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
let h_pubkey = hex::decode(&data.pubkey)?;
|
||||||
|
if h_pubkey.len() != 32 {
|
||||||
|
return ApiData::err("Invalid public key");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut handle = NostrDomainHandle {
|
||||||
|
domain_id: domain.id,
|
||||||
|
handle: data.name.clone(),
|
||||||
|
pubkey: h_pubkey,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let id = db.insert_handle(&handle).await?;
|
||||||
|
handle.id = id;
|
||||||
|
|
||||||
|
ApiData::ok(handle.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[openapi(tag = "NIP05")]
|
||||||
|
#[delete("/api/v1/nostr/domain/<dom>/handle/<handle>")]
|
||||||
|
async fn v1_delete_nostr_domain_handle(
|
||||||
|
auth: Nip98Auth,
|
||||||
|
db: &State<Arc<dyn LNVpsDb>>,
|
||||||
|
dom: u64,
|
||||||
|
handle: u64,
|
||||||
|
) -> ApiResult<()> {
|
||||||
|
let pubkey = auth.event.pubkey.to_bytes();
|
||||||
|
let uid = db.upsert_user(&pubkey).await?;
|
||||||
|
|
||||||
|
let domain = db.get_domain(dom).await?;
|
||||||
|
if domain.owner_id != uid {
|
||||||
|
return ApiData::err("Access denied");
|
||||||
|
}
|
||||||
|
db.delete_handle(handle).await?;
|
||||||
|
ApiData::ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, JsonSchema)]
|
||||||
|
struct NameRequest {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, JsonSchema)]
|
||||||
|
struct HandleRequest {
|
||||||
|
pub pubkey: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, JsonSchema)]
|
||||||
|
struct ApiNostrDomain {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub handles: u64,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub relays: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NostrDomain> for ApiNostrDomain {
|
||||||
|
fn from(value: NostrDomain) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
enabled: value.enabled,
|
||||||
|
handles: value.handles as u64,
|
||||||
|
created: value.created,
|
||||||
|
relays: if let Some(r) = value.relays {
|
||||||
|
r.split(',').map(|s| s.to_string()).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, JsonSchema)]
|
||||||
|
struct ApiNostrDomainHandle {
|
||||||
|
pub id: u64,
|
||||||
|
pub domain_id: u64,
|
||||||
|
pub handle: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub pubkey: String,
|
||||||
|
pub relays: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NostrDomainHandle> for ApiNostrDomainHandle {
|
||||||
|
fn from(value: NostrDomainHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
domain_id: value.domain_id,
|
||||||
|
created: value.created,
|
||||||
|
handle: value.handle,
|
||||||
|
pubkey: hex::encode(value.pubkey),
|
||||||
|
relays: if let Some(r) = value.relays {
|
||||||
|
r.split(',').map(|s| s.to_string()).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, JsonSchema)]
|
||||||
|
struct ApiDomainsResponse {
|
||||||
|
pub domains: Vec<ApiNostrDomain>,
|
||||||
|
pub cname: String,
|
||||||
|
}
|
@ -25,7 +25,7 @@ use rocket::{get, patch, post, routes, Responder, Route, State};
|
|||||||
use rocket_okapi::gen::OpenApiGenerator;
|
use rocket_okapi::gen::OpenApiGenerator;
|
||||||
use rocket_okapi::okapi::openapi3::Responses;
|
use rocket_okapi::okapi::openapi3::Responses;
|
||||||
use rocket_okapi::response::OpenApiResponderInner;
|
use rocket_okapi::response::OpenApiResponderInner;
|
||||||
use rocket_okapi::{openapi, openapi_get_routes};
|
use rocket_okapi::{openapi, openapi_get_routes, openapi_routes};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ssh_key::PublicKey;
|
use ssh_key::PublicKey;
|
||||||
@ -38,7 +38,7 @@ use tokio::sync::mpsc::{Sender, UnboundedSender};
|
|||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut routes = vec![];
|
let mut routes = vec![];
|
||||||
|
|
||||||
routes.append(&mut openapi_get_routes![
|
let mut api_routes = openapi_get_routes![
|
||||||
v1_get_account,
|
v1_get_account,
|
||||||
v1_patch_account,
|
v1_patch_account,
|
||||||
v1_list_vms,
|
v1_list_vms,
|
||||||
@ -59,17 +59,20 @@ pub fn routes() -> Vec<Route> {
|
|||||||
v1_custom_template_calc,
|
v1_custom_template_calc,
|
||||||
v1_create_custom_vm_order,
|
v1_create_custom_vm_order,
|
||||||
v1_get_payment_methods
|
v1_get_payment_methods
|
||||||
]);
|
];
|
||||||
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
api_routes.append(&mut super::nostr_domain::routes());
|
||||||
|
routes.append(&mut api_routes);
|
||||||
|
|
||||||
routes.append(&mut routes![v1_terminal_proxy]);
|
routes.append(&mut routes![v1_terminal_proxy]);
|
||||||
|
|
||||||
routes
|
routes
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>;
|
pub type ApiResult<T> = Result<Json<ApiData<T>>, ApiError>;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
struct ApiData<T: Serialize> {
|
pub struct ApiData<T: Serialize> {
|
||||||
pub data: T,
|
pub data: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +87,7 @@ impl<T: Serialize> ApiData<T> {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Responder)]
|
#[derive(Serialize, Deserialize, JsonSchema, Responder)]
|
||||||
#[response(status = 500)]
|
#[response(status = 500)]
|
||||||
struct ApiError {
|
pub struct ApiError {
|
||||||
pub error: String,
|
pub error: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use config::{Config, File};
|
use config::{Config, File};
|
||||||
use lnvps::api;
|
use lnvps_api::api;
|
||||||
use lnvps::cors::CORS;
|
use lnvps_api::data_migration::run_data_migrations;
|
||||||
use lnvps::data_migration::run_data_migrations;
|
use lnvps_api::dvm::start_dvms;
|
||||||
use lnvps::dvm::start_dvms;
|
use lnvps_api::exchange::{DefaultRateCache, ExchangeRateService};
|
||||||
use lnvps::exchange::{DefaultRateCache, ExchangeRateService};
|
use lnvps_api::lightning::get_node;
|
||||||
use lnvps::lightning::get_node;
|
use lnvps_api::payments::listen_all_payments;
|
||||||
use lnvps::payments::listen_all_payments;
|
use lnvps_api::settings::Settings;
|
||||||
use lnvps::settings::Settings;
|
use lnvps_api::status::VmStateCache;
|
||||||
use lnvps::status::VmStateCache;
|
use lnvps_api::worker::{WorkJob, Worker};
|
||||||
use lnvps::worker::{WorkJob, Worker};
|
use lnvps_common::CORS;
|
||||||
use lnvps_db::{LNVpsDb, LNVpsDbMysql};
|
use lnvps_db::{LNVpsDb, LNVpsDbMysql};
|
||||||
use log::error;
|
use log::error;
|
||||||
use nostr::Keys;
|
use nostr::Keys;
|
||||||
@ -168,7 +168,7 @@ async fn main() -> Result<(), Error> {
|
|||||||
.launch()
|
.launch()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
error!("{}", e);
|
error!("{:?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
use crate::data_migration::DataMigration;
|
use crate::data_migration::DataMigration;
|
||||||
use crate::provisioner::{LNVpsProvisioner, NetworkProvisioner};
|
use crate::provisioner::{LNVpsProvisioner, NetworkProvisioner};
|
||||||
|
use chrono::Utc;
|
||||||
use ipnetwork::IpNetwork;
|
use ipnetwork::IpNetwork;
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
|
use log::info;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use chrono::Utc;
|
|
||||||
use log::info;
|
|
||||||
|
|
||||||
pub struct Ip6InitDataMigration {
|
pub struct Ip6InitDataMigration {
|
||||||
db: Arc<dyn LNVpsDb>,
|
db: Arc<dyn LNVpsDb>,
|
||||||
@ -35,11 +35,10 @@ impl DataMigration for Ip6InitDataMigration {
|
|||||||
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
let ips = db.list_vm_ip_assignments(vm.id).await?;
|
||||||
// if no ipv6 address is picked already pick one
|
// if no ipv6 address is picked already pick one
|
||||||
if ips.iter().all(|i| {
|
if ips.iter().all(|i| {
|
||||||
IpNetwork::from_str(&i.ip)
|
IpNetwork::from_str(&i.ip)
|
||||||
.map(|i| i.is_ipv4())
|
.map(|i| i.is_ipv4())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
}) {
|
||||||
{
|
|
||||||
let ips_pick = net.pick_ip_for_region(host.region_id).await?;
|
let ips_pick = net.pick_ip_for_region(host.region_id).await?;
|
||||||
if let Some(mut v6) = ips_pick.ip6 {
|
if let Some(mut v6) = ips_pick.ip6 {
|
||||||
info!("Assigning ip {} to vm {}", v6.ip, vm.id);
|
info!("Assigning ip {} to vm {}", v6.ip, vm.id);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use crate::data_migration::dns::DnsDataMigration;
|
use crate::data_migration::dns::DnsDataMigration;
|
||||||
|
use crate::data_migration::ip6_init::Ip6InitDataMigration;
|
||||||
|
use crate::provisioner::LNVpsProvisioner;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use lnvps_db::LNVpsDb;
|
use lnvps_db::LNVpsDb;
|
||||||
@ -6,8 +8,6 @@ use log::{error, info};
|
|||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::data_migration::ip6_init::Ip6InitDataMigration;
|
|
||||||
use crate::provisioner::LNVpsProvisioner;
|
|
||||||
|
|
||||||
mod dns;
|
mod dns;
|
||||||
mod ip6_init;
|
mod ip6_init;
|
||||||
@ -17,9 +17,16 @@ pub trait DataMigration: Send + Sync {
|
|||||||
fn migrate(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send>>;
|
fn migrate(&self) -> Pin<Box<dyn Future<Output = Result<()>> + Send>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_data_migrations(db: Arc<dyn LNVpsDb>, lnvps: Arc<LNVpsProvisioner>, settings: &Settings) -> Result<()> {
|
pub async fn run_data_migrations(
|
||||||
|
db: Arc<dyn LNVpsDb>,
|
||||||
|
lnvps: Arc<LNVpsProvisioner>,
|
||||||
|
settings: &Settings,
|
||||||
|
) -> Result<()> {
|
||||||
let mut migrations: Vec<Box<dyn DataMigration>> = vec![];
|
let mut migrations: Vec<Box<dyn DataMigration>> = vec![];
|
||||||
migrations.push(Box::new(Ip6InitDataMigration::new(db.clone(), lnvps.clone())));
|
migrations.push(Box::new(Ip6InitDataMigration::new(
|
||||||
|
db.clone(),
|
||||||
|
lnvps.clone(),
|
||||||
|
)));
|
||||||
|
|
||||||
if let Some(d) = DnsDataMigration::new(db.clone(), settings) {
|
if let Some(d) = DnsDataMigration::new(db.clone(), settings) {
|
||||||
migrations.push(Box::new(d));
|
migrations.push(Box::new(d));
|
||||||
|
@ -6,9 +6,9 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
#[cfg(feature = "cloudflare")]
|
#[cfg(feature = "cloudflare")]
|
||||||
mod cloudflare;
|
mod cloudflare;
|
||||||
|
use crate::provisioner::NetworkProvisioner;
|
||||||
#[cfg(feature = "cloudflare")]
|
#[cfg(feature = "cloudflare")]
|
||||||
pub use cloudflare::*;
|
pub use cloudflare::*;
|
||||||
use crate::provisioner::NetworkProvisioner;
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait DnsServer: Send + Sync {
|
pub trait DnsServer: Send + Sync {
|
||||||
|
@ -221,10 +221,13 @@ pub struct VmHostDiskInfo {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::Utc;
|
|
||||||
use lnvps_db::{DiskInterface, DiskType, IpRange, IpRangeAllocationMode, OsDistribution, UserSshKey, Vm, VmHost, VmHostDisk, VmIpAssignment, VmOsImage, VmTemplate};
|
|
||||||
use crate::{GB, TB};
|
|
||||||
use crate::host::FullVmInfo;
|
use crate::host::FullVmInfo;
|
||||||
|
use crate::{GB, TB};
|
||||||
|
use chrono::Utc;
|
||||||
|
use lnvps_db::{
|
||||||
|
DiskInterface, DiskType, IpRange, IpRangeAllocationMode, OsDistribution, UserSshKey, Vm,
|
||||||
|
VmHost, VmHostDisk, VmIpAssignment, VmOsImage, VmTemplate,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn mock_full_vm() -> FullVmInfo {
|
pub fn mock_full_vm() -> FullVmInfo {
|
||||||
let template = VmTemplate {
|
let template = VmTemplate {
|
||||||
@ -367,4 +370,4 @@ mod tests {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
pub mod cors;
|
|
||||||
pub mod data_migration;
|
pub mod data_migration;
|
||||||
pub mod dns;
|
pub mod dns;
|
||||||
pub mod exchange;
|
pub mod exchange;
|
||||||
|
@ -11,10 +11,10 @@ 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, DiskInterface, DiskType, IpRange, IpRangeAllocationMode, LNVpsDb,
|
async_trait, AccessPolicy, DiskInterface, DiskType, IpRange, IpRangeAllocationMode,
|
||||||
OsDistribution, User, UserSshKey, Vm, VmCostPlan, VmCostPlanIntervalType, VmCustomPricing,
|
LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, OsDistribution, User, UserSshKey, Vm,
|
||||||
VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostKind, VmHostRegion,
|
VmCostPlan, VmCostPlanIntervalType, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate,
|
||||||
VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
VmHost, VmHostDisk, VmHostKind, VmHostRegion, VmIpAssignment, VmOsImage, VmPayment, VmTemplate,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
@ -206,6 +206,57 @@ impl Default for MockDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl LNVPSNostrDb for MockDb {
|
||||||
|
async fn get_handle(&self, handle_id: u64) -> anyhow::Result<NostrDomainHandle> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_handle_by_name(
|
||||||
|
&self,
|
||||||
|
domain_id: u64,
|
||||||
|
handle: &str,
|
||||||
|
) -> anyhow::Result<NostrDomainHandle> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_handle(&self, handle: &NostrDomainHandle) -> anyhow::Result<u64> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_handle(&self, handle: &NostrDomainHandle) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_handle(&self, handle_id: u64) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_handles(&self, domain_id: u64) -> anyhow::Result<Vec<NostrDomainHandle>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_domain(&self, id: u64) -> anyhow::Result<NostrDomain> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_domain_by_name(&self, name: &str) -> anyhow::Result<NostrDomain> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_domains(&self, owner_id: u64) -> anyhow::Result<Vec<NostrDomain>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_domain(&self, domain: &NostrDomain) -> anyhow::Result<u64> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_domain(&self, domain_id: u64) -> anyhow::Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl LNVpsDb for MockDb {
|
impl LNVpsDb for MockDb {
|
||||||
async fn migrate(&self) -> anyhow::Result<()> {
|
async fn migrate(&self) -> anyhow::Result<()> {
|
||||||
|
@ -30,7 +30,10 @@ pub struct ArpEntry {
|
|||||||
|
|
||||||
impl ArpEntry {
|
impl ArpEntry {
|
||||||
pub fn new(vm: &Vm, ip: &VmIpAssignment, interface: Option<String>) -> Result<Self> {
|
pub fn new(vm: &Vm, ip: &VmIpAssignment, interface: Option<String>) -> Result<Self> {
|
||||||
ensure!(vm.mac_address != "ff:ff:ff:ff:ff:ff", "MAC address is invalid because its blank");
|
ensure!(
|
||||||
|
vm.mac_address != "ff:ff:ff:ff:ff:ff",
|
||||||
|
"MAC address is invalid because its blank"
|
||||||
|
);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: ip.arp_ref.clone(),
|
id: ip.arp_ref.clone(),
|
||||||
address: ip.ip.clone(),
|
address: ip.ip.clone(),
|
||||||
|
@ -351,5 +351,5 @@ enum OvhTaskFunction {
|
|||||||
MoveVirtualMac,
|
MoveVirtualMac,
|
||||||
VirtualMacAdd,
|
VirtualMacAdd,
|
||||||
VirtualMacDelete,
|
VirtualMacDelete,
|
||||||
RemoveVirtualMac
|
RemoveVirtualMac,
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,9 @@ pub struct Settings {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
/// Tax rates to change per country as a percent of the amount
|
/// Tax rates to change per country as a percent of the amount
|
||||||
pub tax_rate: HashMap<CountryCode, f32>,
|
pub tax_rate: HashMap<CountryCode, f32>,
|
||||||
|
|
||||||
|
/// public host of lnvps_nostr service
|
||||||
|
pub nostr_address_host: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
@ -237,5 +240,6 @@ pub fn mock_settings() -> Settings {
|
|||||||
nostr: None,
|
nostr: None,
|
||||||
revolut: None,
|
revolut: None,
|
||||||
tax_rate: HashMap::from([(CountryCode::IRL, 23.0), (CountryCode::USA, 1.0)]),
|
tax_rate: HashMap::from([(CountryCode::IRL, 23.0), (CountryCode::USA, 1.0)]),
|
||||||
|
nostr_address_host: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
lnvps_common/Cargo.toml
Normal file
7
lnvps_common/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "lnvps_common"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rocket.workspace = true
|
2
lnvps_common/src/lib.rs
Normal file
2
lnvps_common/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod cors;
|
||||||
|
pub use cors::*;
|
@ -6,6 +6,7 @@ edition = "2021"
|
|||||||
[features]
|
[features]
|
||||||
default = ["mysql"]
|
default = ["mysql"]
|
||||||
mysql = ["sqlx/mysql"]
|
mysql = ["sqlx/mysql"]
|
||||||
|
nostr-domain = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
25
lnvps_db/migrations/20250402115943_nostr_address.sql
Normal file
25
lnvps_db/migrations/20250402115943_nostr_address.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
create table nostr_domain
|
||||||
|
(
|
||||||
|
id integer unsigned not null auto_increment primary key,
|
||||||
|
owner_id integer unsigned not null,
|
||||||
|
name varchar(200) not null,
|
||||||
|
enabled bit(1) not null default 0,
|
||||||
|
created timestamp not null default current_timestamp,
|
||||||
|
relays varchar(1024),
|
||||||
|
|
||||||
|
unique key ix_domain_unique (name),
|
||||||
|
constraint fk_nostr_domain_user foreign key (owner_id) references users (id)
|
||||||
|
);
|
||||||
|
create table nostr_domain_handle
|
||||||
|
(
|
||||||
|
id integer unsigned not null auto_increment primary key,
|
||||||
|
domain_id integer unsigned not null,
|
||||||
|
handle varchar(100) not null,
|
||||||
|
created timestamp not null default current_timestamp,
|
||||||
|
pubkey binary(32) not null,
|
||||||
|
relays varchar(1024),
|
||||||
|
|
||||||
|
unique key ix_domain_handle_unique (domain_id, handle),
|
||||||
|
constraint fk_nostr_domain_handle_domain foreign key (domain_id) references nostr_domain (id) on delete cascade
|
||||||
|
)
|
@ -10,7 +10,7 @@ pub use mysql::*;
|
|||||||
pub use async_trait::async_trait;
|
pub use async_trait::async_trait;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait LNVpsDb: Sync + Send {
|
pub trait LNVpsDb: LNVPSNostrDb + Send + Sync {
|
||||||
/// Migrate database
|
/// Migrate database
|
||||||
async fn migrate(&self) -> Result<()>;
|
async fn migrate(&self) -> Result<()>;
|
||||||
|
|
||||||
@ -173,3 +173,40 @@ pub trait LNVpsDb: Sync + Send {
|
|||||||
/// 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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
#[async_trait]
|
||||||
|
pub trait LNVPSNostrDb: Sync + Send {
|
||||||
|
/// Get single handle for a domain
|
||||||
|
async fn get_handle(&self, handle_id: u64) -> Result<NostrDomainHandle>;
|
||||||
|
|
||||||
|
/// Get single handle for a domain
|
||||||
|
async fn get_handle_by_name(&self, domain_id: u64, handle: &str) -> Result<NostrDomainHandle>;
|
||||||
|
|
||||||
|
/// Insert a new handle
|
||||||
|
async fn insert_handle(&self, handle: &NostrDomainHandle) -> Result<u64>;
|
||||||
|
|
||||||
|
/// Update an existing domain handle
|
||||||
|
async fn update_handle(&self, handle: &NostrDomainHandle) -> Result<()>;
|
||||||
|
|
||||||
|
/// Delete handle entry
|
||||||
|
async fn delete_handle(&self, handle_id: u64) -> Result<()>;
|
||||||
|
|
||||||
|
/// List handles
|
||||||
|
async fn list_handles(&self, domain_id: u64) -> Result<Vec<NostrDomainHandle>>;
|
||||||
|
|
||||||
|
/// Get domain object by id
|
||||||
|
async fn get_domain(&self, id: u64) -> Result<NostrDomain>;
|
||||||
|
|
||||||
|
/// Get domain object by name
|
||||||
|
async fn get_domain_by_name(&self, name: &str) -> Result<NostrDomain>;
|
||||||
|
|
||||||
|
/// List domains owned by a user
|
||||||
|
async fn list_domains(&self, owner_id: u64) -> Result<Vec<NostrDomain>>;
|
||||||
|
|
||||||
|
/// Insert a new domain
|
||||||
|
async fn insert_domain(&self, domain: &NostrDomain) -> Result<u64>;
|
||||||
|
|
||||||
|
/// Delete a domain
|
||||||
|
async fn delete_domain(&self, domain_id: u64) -> Result<()>;
|
||||||
|
}
|
||||||
|
@ -489,3 +489,24 @@ impl FromStr for PaymentMethod {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
|
pub struct NostrDomain {
|
||||||
|
pub id: u64,
|
||||||
|
pub owner_id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub relays: Option<String>,
|
||||||
|
pub handles: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow, Clone, Debug, Default)]
|
||||||
|
pub struct NostrDomainHandle {
|
||||||
|
pub id: u64,
|
||||||
|
pub domain_id: u64,
|
||||||
|
pub handle: String,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub pubkey: Vec<u8>,
|
||||||
|
pub relays: Option<String>,
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
AccessPolicy, IpRange, LNVpsDb, Router, User, UserSshKey, Vm, VmCostPlan, VmCustomPricing,
|
AccessPolicy, IpRange, LNVPSNostrDb, LNVpsDb, NostrDomain, NostrDomainHandle, Router, User,
|
||||||
VmCustomPricingDisk, VmCustomTemplate, VmHost, VmHostDisk, VmHostRegion, VmIpAssignment,
|
UserSshKey, Vm, VmCostPlan, VmCustomPricing, VmCustomPricingDisk, VmCustomTemplate, VmHost,
|
||||||
VmOsImage, VmPayment, VmTemplate,
|
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;
|
||||||
@ -556,3 +556,115 @@ impl LNVpsDb for LNVpsDbMysql {
|
|||||||
.map_err(Error::new)
|
.map_err(Error::new)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nostr-domain")]
|
||||||
|
#[async_trait]
|
||||||
|
impl LNVPSNostrDb for LNVpsDbMysql {
|
||||||
|
async fn get_handle(&self, handle_id: u64) -> Result<NostrDomainHandle> {
|
||||||
|
sqlx::query_as("select * from nostr_domain_handle where id=?")
|
||||||
|
.bind(handle_id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_handle_by_name(&self, domain_id: u64, handle: &str) -> Result<NostrDomainHandle> {
|
||||||
|
sqlx::query_as("select * from nostr_domain_handle where domain_id=? and handle=?")
|
||||||
|
.bind(domain_id)
|
||||||
|
.bind(handle)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_handle(&self, handle: &NostrDomainHandle) -> Result<u64> {
|
||||||
|
Ok(
|
||||||
|
sqlx::query(
|
||||||
|
"insert into nostr_domain_handle(domain_id,handle,pubkey,relays) values(?,?,?,?) returning id",
|
||||||
|
)
|
||||||
|
.bind(handle.domain_id)
|
||||||
|
.bind(&handle.handle)
|
||||||
|
.bind(&handle.pubkey)
|
||||||
|
.bind(&handle.relays)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)?
|
||||||
|
.try_get(0)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_handle(&self, handle: &NostrDomainHandle) -> Result<()> {
|
||||||
|
sqlx::query("update nostr_domain_handle set handle=?,pubkey=?,relays=? where id=?")
|
||||||
|
.bind(&handle.handle)
|
||||||
|
.bind(&handle.pubkey)
|
||||||
|
.bind(&handle.relays)
|
||||||
|
.bind(handle.id)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_handle(&self, handle_id: u64) -> Result<()> {
|
||||||
|
sqlx::query("delete from nostr_domain_handle where id=?")
|
||||||
|
.bind(handle_id)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_handles(&self, domain_id: u64) -> Result<Vec<NostrDomainHandle>> {
|
||||||
|
sqlx::query_as("select * from nostr_domain_handle where domain_id=?")
|
||||||
|
.bind(domain_id)
|
||||||
|
.fetch_all(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_domain(&self, id: u64) -> Result<NostrDomain> {
|
||||||
|
sqlx::query_as("select *,(select count(1) from nostr_domain_handle where domain_id=nostr_domain.id) handles from nostr_domain where id=?")
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_domain_by_name(&self, name: &str) -> Result<NostrDomain> {
|
||||||
|
sqlx::query_as("select *,(select count(1) from nostr_domain_handle where domain_id=nostr_domain.id) handles from nostr_domain where name=?")
|
||||||
|
.bind(name)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_domains(&self, owner_id: u64) -> Result<Vec<NostrDomain>> {
|
||||||
|
sqlx::query_as("select *,(select count(1) from nostr_domain_handle where domain_id=nostr_domain.id) handles from nostr_domain where owner_id=?")
|
||||||
|
.bind(owner_id)
|
||||||
|
.fetch_all(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_domain(&self, domain: &NostrDomain) -> Result<u64> {
|
||||||
|
Ok(
|
||||||
|
sqlx::query(
|
||||||
|
"insert into nostr_domain(owner_id,name,relays) values(?,?,?) returning id",
|
||||||
|
)
|
||||||
|
.bind(domain.owner_id)
|
||||||
|
.bind(&domain.name)
|
||||||
|
.bind(&domain.relays)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)?
|
||||||
|
.try_get(0)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_domain(&self, domain_id: u64) -> Result<()> {
|
||||||
|
sqlx::query("update nostr_domain set deleted = current_timestamp where id = ?")
|
||||||
|
.bind(domain_id)
|
||||||
|
.fetch_one(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(Error::new)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,4 +4,14 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lnvps_db = { path = "../lnvps_db" }
|
lnvps_db = { path = "../lnvps_db", features = ["nostr-domain"] }
|
||||||
|
lnvps_common = { path = "../lnvps_common" }
|
||||||
|
env_logger.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
config.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
rocket.workspace = true
|
||||||
|
hex.workspace = true
|
||||||
|
3
lnvps_nostr/README.md
Normal file
3
lnvps_nostr/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# LNVPS Nostr Services
|
||||||
|
|
||||||
|
A simple webserver hosting various nostr based services for lnvps.net
|
5
lnvps_nostr/config.yaml
Normal file
5
lnvps_nostr/config.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Connection string to lnvps database
|
||||||
|
db: "mysql://root:root@localhost:3376/lnvps"
|
||||||
|
|
||||||
|
# Listen address for http server
|
||||||
|
listen: "127.0.0.1:8001"
|
@ -1,3 +1,65 @@
|
|||||||
fn main() {
|
mod routes;
|
||||||
println!("Hello, world!");
|
|
||||||
|
use crate::routes::routes;
|
||||||
|
use anyhow::Result;
|
||||||
|
use config::{Config, File};
|
||||||
|
use lnvps_common::CORS;
|
||||||
|
use lnvps_db::{LNVPSNostrDb, LNVpsDbMysql};
|
||||||
|
use log::error;
|
||||||
|
use rocket::http::Method;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
struct Settings {
|
||||||
|
/// Database connection string
|
||||||
|
db: String,
|
||||||
|
/// Listen address for http server
|
||||||
|
listen: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let settings: Settings = Config::builder()
|
||||||
|
.add_source(File::from(PathBuf::from("config.yaml")))
|
||||||
|
.build()?
|
||||||
|
.try_deserialize()?;
|
||||||
|
|
||||||
|
// Connect database
|
||||||
|
let db = LNVpsDbMysql::new(&settings.db).await?;
|
||||||
|
let db: Arc<dyn LNVPSNostrDb> = Arc::new(db);
|
||||||
|
|
||||||
|
let mut config = rocket::Config::default();
|
||||||
|
let ip: SocketAddr = match &settings.listen {
|
||||||
|
Some(i) => i.parse()?,
|
||||||
|
None => SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 8000),
|
||||||
|
};
|
||||||
|
config.address = ip.ip();
|
||||||
|
config.port = ip.port();
|
||||||
|
|
||||||
|
if let Err(e) = rocket::Rocket::custom(config)
|
||||||
|
.manage(db.clone())
|
||||||
|
.manage(settings.clone())
|
||||||
|
.attach(CORS)
|
||||||
|
.mount("/", routes())
|
||||||
|
.mount(
|
||||||
|
"/",
|
||||||
|
vec![rocket::Route::ranked(
|
||||||
|
isize::MAX,
|
||||||
|
Method::Options,
|
||||||
|
"/<catch_all_options_route..>",
|
||||||
|
CORS,
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
.launch()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("{}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
65
lnvps_nostr/src/routes.rs
Normal file
65
lnvps_nostr/src/routes.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
use lnvps_db::LNVPSNostrDb;
|
||||||
|
use log::info;
|
||||||
|
use rocket::request::{FromRequest, Outcome};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket::{Request, Route, State, routes};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![nostr_address]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct NostrJson {
|
||||||
|
pub names: HashMap<String, String>,
|
||||||
|
pub relays: HashMap<String, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HostInfo<'r> {
|
||||||
|
pub host: Option<&'r str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for HostInfo<'r> {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
Outcome::Success(HostInfo {
|
||||||
|
host: request.host().map(|h| h.domain().as_str()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/.well-known/nostr.json?<name>")]
|
||||||
|
async fn nostr_address(
|
||||||
|
host: HostInfo<'_>,
|
||||||
|
db: &State<Arc<dyn LNVPSNostrDb>>,
|
||||||
|
name: Option<&str>,
|
||||||
|
) -> Result<Json<NostrJson>, &'static str> {
|
||||||
|
let name = name.unwrap_or("_");
|
||||||
|
let host = host.host.unwrap_or("lnvps.net");
|
||||||
|
info!("Got request for {} on host {}", name, host);
|
||||||
|
let domain = db
|
||||||
|
.get_domain_by_name(host)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Domain not found")?;
|
||||||
|
let handle = db
|
||||||
|
.get_handle_by_name(domain.id, name)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Handle not found")?;
|
||||||
|
|
||||||
|
let pubkey_hex = hex::encode(handle.pubkey);
|
||||||
|
let relays = if let Some(r) = handle.relays {
|
||||||
|
r.split(",").map(|x| x.to_string()).collect()
|
||||||
|
} else if let Some(r) = domain.relays {
|
||||||
|
r.split(",").map(|x| x.to_string()).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
Ok(Json(NostrJson {
|
||||||
|
names: HashMap::from([(name.to_string(), pubkey_hex.clone())]),
|
||||||
|
relays: HashMap::from([(pubkey_hex, relays)]),
|
||||||
|
}))
|
||||||
|
}
|
Reference in New Issue
Block a user