diff --git a/.env.sample b/.env.sample index 5c0e008..55e5d92 100644 --- a/.env.sample +++ b/.env.sample @@ -7,3 +7,6 @@ export BITCOIN_RPC_USER="polaruser" export BITCOIN_RPC_PASSWORD="polarpass" export NETWORK="regtest" export NSEC="my_nsec" +export JWT_SECRET="my_jwt_secret" +export GITHUB_CLIENT_ID="my_github_client_id" +export GITHUB_CLIENT_SECRET="my_github_client_secret" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8ed9373..fe12047 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -306,6 +321,12 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -392,32 +413,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitcoincore-rpc" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6c0ee9354e3dac217db4cb1dd31941073a87fe53c86bcf3eb2b8bc97f00a08" -dependencies = [ - "bitcoin-private", - "bitcoincore-rpc-json", - "jsonrpc", - "log", - "serde", - "serde_json", -] - -[[package]] -name = "bitcoincore-rpc-json" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d30ce6f40fb0a2e8d98522796219282504b7a4b14e2b4c26139a7bea6aec6586" -dependencies = [ - "bitcoin", - "bitcoin-private", - "serde", - "serde_json", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -514,6 +509,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", +] + [[package]] name = "cipher" version = "0.4.4" @@ -585,6 +594,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -1115,6 +1133,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1218,14 +1259,18 @@ dependencies = [ ] [[package]] -name = "jsonrpc" -version = "0.14.1" +name = "jsonwebtoken" +version = "9.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8128f36b47411cd3f044be8c1f5cc0c9e24d1d1bfdc45f0a57897b32513053f2" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" dependencies = [ - "base64 0.13.1", + "base64 0.21.4", + "js-sys", + "pem", + "ring", "serde", "serde_json", + "simple_asn1", ] [[package]] @@ -1396,18 +1441,21 @@ name = "mutinynet-faucet-rs" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "axum 0.6.20", "bitcoin", "bitcoin-waila", - "bitcoincore-rpc", + "chrono", "dotenv", "hex", + "jsonwebtoken", "lightning-invoice", "lnurl-rs", "log", "nostr 0.29.0", "nostr-sdk", "pretty_env_logger 0.5.0", + "reqwest", "serde", "serde_json", "tokio", @@ -1564,11 +1612,36 @@ dependencies = [ "thiserror", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1706,6 +1779,16 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1787,6 +1870,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2317,6 +2406,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -2466,6 +2567,37 @@ dependencies = [ "syn 2.0.32", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -3105,6 +3237,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 1e0ddf8..080e0d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = "0.1" anyhow = "1.0.70" axum = { version = "0.6.20", features = ["macros", "headers"] } bitcoin-waila = "0.3.0" @@ -16,7 +17,6 @@ tonic_openssl_lnd = "0.2.0" dotenv = "0.15.0" lnurl-rs = "0.4.0" hex = "0.4.3" -bitcoincore-rpc = "0.17.0" bitcoin = "0.30.2" lightning-invoice = "0.27.0" tower-http = { version = "0.4.0", features = ["cors"] } @@ -24,3 +24,6 @@ log = "0.4.20" pretty_env_logger = "0.5.0" nostr = "0.29.0" nostr-sdk = "0.29.0" +jsonwebtoken = "9.3.0" +reqwest = "0.11.23" +chrono = "0.4.39" diff --git a/Dockerfile b/Dockerfile index 44aa2e2..41b1076 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,18 @@ FROM rust:1.76.0 AS builder -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends clang cmake build-essential +# Install build dependencies +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + clang \ + cmake \ + build-essential \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app +# Copy source code COPY . . +# Build the application RUN cargo build --release ENTRYPOINT ["/bin/bash", "-c", "./target/release/mutinynet-faucet-rs ${FLAGS}"] diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..b1bbbca --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,110 @@ +use crate::AppState; +use axum::headers::authorization::Bearer; +use axum::headers::Authorization; +use axum::http::{Request, StatusCode}; +use axum::middleware::Next; +use axum::response::{IntoResponse, Response}; +use axum::{Json, TypedHeader}; +use jsonwebtoken::{decode, DecodingKey, Validation}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +pub struct AuthState { + pub client: Client, + pub github_client_id: String, + pub github_client_secret: String, + pub jwt_secret: String, +} + +#[derive(Deserialize)] +pub struct GithubCallback { + pub code: String, +} + +#[derive(Serialize, Deserialize)] +pub struct TokenClaims { + pub sub: String, + pub exp: usize, + pub iat: usize, +} + +#[derive(Deserialize)] +pub struct GithubTokenResponse { + pub access_token: String, +} + +#[derive(Deserialize)] +pub struct GithubUser { + pub id: i64, + pub login: String, +} + +// Custom error type for auth failures +#[derive(Debug, Clone, Copy)] +pub enum AuthError { + InvalidToken, + MissingToken, + TokenExpired, +} + +impl IntoResponse for AuthError { + fn into_response(self) -> Response { + let (status, message) = match self { + AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token"), + AuthError::MissingToken => (StatusCode::UNAUTHORIZED, "Missing token"), + AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired"), + }; + + ( + status, + Json(serde_json::json!({ + "error": message + })), + ) + .into_response() + } +} + +// Middleware extractor for authenticated users +#[derive(Debug, Clone)] +pub struct AuthUser { + pub username: String, +} + +// Middleware for JWT verification +pub async fn auth_middleware( + TypedHeader(auth): TypedHeader>, + mut request: Request, + next: Next, +) -> Result { + let state = request + .extensions() + .get::() + .expect("JWT config not found in extensions"); + + if auth.token().is_empty() { + return Err(AuthError::MissingToken); + } + + // Verify and decode the token + let token_data = decode::( + auth.token(), + &DecodingKey::from_secret(state.auth.jwt_secret.as_bytes()), + &Validation::default(), + ) + .map_err(|_| AuthError::InvalidToken)?; + + // Check if token is expired + let now = chrono::Utc::now().timestamp() as usize; + if token_data.claims.exp < now { + return Err(AuthError::TokenExpired); + } + + // Add AuthUser to request extensions + request.extensions_mut().insert(AuthUser { + username: token_data.claims.sub, + }); + + Ok(next.run(request).await) +} diff --git a/src/bolt11.rs b/src/bolt11.rs index 72f79f3..eb8809d 100644 --- a/src/bolt11.rs +++ b/src/bolt11.rs @@ -13,7 +13,7 @@ pub struct Bolt11Response { pub bolt11: String, } -pub async fn request_bolt11(state: AppState, payload: Bolt11Request) -> anyhow::Result { +pub async fn request_bolt11(state: &AppState, payload: Bolt11Request) -> anyhow::Result { let bolt11 = { let mut lightning_client = state.lightning_client.clone(); diff --git a/src/channel.rs b/src/channel.rs index dbf6185..6124932 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -18,7 +18,7 @@ pub struct ChannelResponse { } pub async fn open_channel( - state: AppState, + state: &AppState, x_forwarded_for: &str, payload: ChannelRequest, ) -> anyhow::Result { @@ -90,7 +90,7 @@ pub async fn open_channel( state .payments - .add_payment(x_forwarded_for, payload.capacity as u64) + .add_payment(x_forwarded_for, None, None, payload.capacity as u64) .await; Ok(txid) diff --git a/src/lightning.rs b/src/lightning.rs index f11d64b..2af7ce8 100644 --- a/src/lightning.rs +++ b/src/lightning.rs @@ -25,7 +25,7 @@ pub struct LightningResponse { } pub async fn pay_lightning( - state: AppState, + state: &AppState, x_forwarded_for: &str, bolt11: &str, ) -> anyhow::Result { @@ -109,6 +109,8 @@ pub async fn pay_lightning( .payments .add_payment( x_forwarded_for, + None, + None, invoice.amount_milli_satoshis().unwrap_or(0) / 1000, ) .await; diff --git a/src/main.rs b/src/main.rs index e8ab938..36d171a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,16 @@ use axum::extract::Query; use axum::headers::{HeaderMap, HeaderValue}; use axum::http::Uri; +use axum::response::Redirect; use axum::{ http::StatusCode, + middleware, response::{IntoResponse, Response}, routing::{get, post}, Extension, Json, Router, }; +use bitcoin_waila::PaymentParams; +use jsonwebtoken::{encode, EncodingKey, Header}; use lnurl::withdraw::WithdrawalResponse; use lnurl::{AsyncClient, Tag}; use log::error; @@ -14,11 +18,13 @@ use nostr::key::Keys; use serde::Deserialize; use serde_json::{json, Value}; use std::net::SocketAddr; +use std::str::FromStr; use tokio::signal::unix::{signal, SignalKind}; use tokio::sync::oneshot; use tonic_openssl_lnd::LndLightningClient; -use tower_http::cors::{AllowHeaders, AllowMethods, Any, CorsLayer}; +use tower_http::cors::{AllowMethods, Any, CorsLayer}; +use crate::auth::{auth_middleware, AuthState, AuthUser, GithubCallback}; use crate::nostr_dms::listen_to_nostr_dms; use crate::payments::PaymentsByIp; use bolt11::{request_bolt11, Bolt11Request, Bolt11Response}; @@ -27,6 +33,7 @@ use lightning::{pay_lightning, LightningRequest, LightningResponse}; use onchain::{pay_onchain, OnchainRequest, OnchainResponse}; use setup::setup; +mod auth; mod bolt11; mod channel; mod lightning; @@ -43,6 +50,7 @@ pub struct AppState { lightning_client: LndLightningClient, lnurl: AsyncClient, payments: PaymentsByIp, + auth: AuthState, } impl AppState { @@ -51,6 +59,7 @@ impl AppState { keys: Keys, lightning_client: LndLightningClient, network: bitcoin::Network, + auth: AuthState, ) -> Self { let lnurl = lnurl::Builder::default().build_async().unwrap(); AppState { @@ -60,6 +69,7 @@ impl AppState { lightning_client, lnurl, payments: PaymentsByIp::new(), + auth, } } } @@ -70,8 +80,13 @@ const MAX_SEND_AMOUNT: u64 = 1_000_000; async fn main() -> anyhow::Result<()> { let state = setup().await?; - let app = Router::new() - .route("/api/onchain", post(onchain_handler)) + let app: Router = Router::new() + .route("/auth/github", get(github_auth)) + .route("/auth/github/callback", get(github_callback)) + .route( + "/api/onchain", + post(onchain_handler).route_layer(middleware::from_fn(auth_middleware)), + ) .route("/api/lightning", post(lightning_handler)) .route("/api/lnurlw", get(lnurlw_handler)) .route("/api/lnurlw/callback", get(lnurlw_callback_handler)) @@ -82,7 +97,7 @@ async fn main() -> anyhow::Result<()> { .layer( CorsLayer::new() .allow_origin(Any) - .allow_headers(AllowHeaders::any()) + .allow_headers([axum::http::header::AUTHORIZATION]) .allow_methods(AllowMethods::any()), ); @@ -140,9 +155,96 @@ async fn main() -> anyhow::Result<()> { Ok(()) } +#[axum::debug_handler] +async fn github_auth(Extension(state): Extension) -> Result { + let redirect_url = format!( + "https://github.com/login/oauth/authorize?client_id={}&scope=user:email&redirect_uri={}/auth/github/callback", + state.auth.github_client_id, + state.host + ); + Ok(Redirect::temporary(&redirect_url)) +} + +#[derive(Deserialize)] +struct GithubEmail { + email: String, + primary: bool, + verified: bool, +} + +#[axum::debug_handler] +async fn github_callback( + Query(params): Query, + Extension(state): Extension, +) -> Result { + // Exchange code for access token + let token_response = state + .auth + .client + .post("https://github.com/login/oauth/access_token") + .header("Accept", "application/json") + .json(&json!({ + "client_id": state.auth.github_client_id, + "client_secret": state.auth.github_client_secret, + "code": params.code, + })) + .send() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .json::() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // Get user info + // Get user's email + let user_emails = state + .auth + .client + .get("https://api.github.com/user/emails") + .header( + "Authorization", + format!("Bearer {}", token_response.access_token), + ) + .header("User-Agent", "rust-github-oauth") + .header("X-GitHub-Api-Version", "2022-11-28") + .send() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .json::>() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // Find primary email + let primary_email = user_emails + .into_iter() + .find(|email| email.primary && email.verified) + .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; + + // Create JWT + let claims = auth::TokenClaims { + sub: primary_email.email, + exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp() as usize, + iat: chrono::Utc::now().timestamp() as usize, + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(state.auth.jwt_secret.as_bytes()), + ) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // Redirect to frontend with token + Ok(Redirect::temporary(&format!( + "{}/?token={token}", + state.host + ))) +} + #[axum::debug_handler] async fn onchain_handler( Extension(state): Extension, + Extension(user): Extension, headers: HeaderMap, Json(payload): Json, ) -> Result, AppError> { @@ -152,15 +254,19 @@ async fn onchain_handler( .and_then(|x| HeaderValue::to_str(x).ok()) .unwrap_or("Unknown"); - if state.payments.get_total_payments(x_forwarded_for).await > MAX_SEND_AMOUNT * 10 { - return Err(AppError::new("Too many payments")); - } + let params = PaymentParams::from_str(&payload.address) + .map_err(|_| anyhow::anyhow!("invalid address"))?; + let address_str = params.address().ok_or(anyhow::anyhow!("invalid address"))?; - if state.payments.get_total_payments(&payload.address).await > MAX_SEND_AMOUNT { + if state + .payments + .verify_payments(x_forwarded_for, Some(&address_str), Some(&user)) + .await + { return Err(AppError::new("Too many payments")); } - let res = pay_onchain(state, x_forwarded_for, payload).await?; + let res = pay_onchain(&state, x_forwarded_for, user, payload).await?; Ok(Json(res)) } @@ -181,7 +287,7 @@ async fn lightning_handler( return Err(AppError::new("Too many payments")); } - let payment_hash = pay_lightning(state, x_forwarded_for, &payload.bolt11).await?; + let payment_hash = pay_lightning(&state, x_forwarded_for, &payload.bolt11).await?; Ok(Json(LightningResponse { payment_hash })) } @@ -223,7 +329,7 @@ async fn lnurlw_callback_handler( return Err(Json(json!({"status": "ERROR", "reason": "Incorrect k1"}))); } - pay_lightning(state, x_forwarded_for, &payload.pr) + pay_lightning(&state, x_forwarded_for, &payload.pr) .await .map_err(|e| Json(json!({"status": "ERROR", "reason": format!("{e}")})))?; Ok(Json(json!({"status": "OK"}))) @@ -237,7 +343,7 @@ async fn bolt11_handler( Extension(state): Extension, Json(payload): Json, ) -> Result, AppError> { - let bolt11 = request_bolt11(state, payload.clone()).await?; + let bolt11 = request_bolt11(&state, payload.clone()).await?; Ok(Json(Bolt11Response { bolt11 })) } @@ -258,7 +364,7 @@ async fn channel_handler( return Err(AppError::new("Too many payments")); } - let txid = open_channel(state, x_forwarded_for, payload).await?; + let txid = open_channel(&state, x_forwarded_for, payload).await?; Ok(Json(ChannelResponse { txid })) } diff --git a/src/nostr_dms.rs b/src/nostr_dms.rs index d0e086d..6c6e081 100644 --- a/src/nostr_dms.rs +++ b/src/nostr_dms.rs @@ -193,13 +193,12 @@ async fn handle_event(event: Event, state: AppState) -> anyhow::Result<()> { state .payments - .add_payment(&event.pubkey.to_string(), amount.to_sat()) - .await; - - // track for address too - state - .payments - .add_payment(&address.to_string(), amount.to_sat()) + .add_payment( + &event.pubkey.to_string(), + Some(&address), + None, + amount.to_sat(), + ) .await; let resp = { diff --git a/src/onchain.rs b/src/onchain.rs index 654e4ad..02cbd47 100644 --- a/src/onchain.rs +++ b/src/onchain.rs @@ -1,3 +1,4 @@ +use crate::auth::AuthUser; use crate::{AppState, MAX_SEND_AMOUNT}; use bitcoin::{Address, Amount}; use bitcoin_waila::PaymentParams; @@ -18,8 +19,9 @@ pub struct OnchainResponse { } pub async fn pay_onchain( - state: AppState, + state: &AppState, x_forwarded_for: &str, + user: AuthUser, payload: OnchainRequest, ) -> anyhow::Result { let res = { @@ -53,13 +55,12 @@ pub async fn pay_onchain( state .payments - .add_payment(x_forwarded_for, amount.to_sat()) - .await; - - // track for address too - state - .payments - .add_payment(&address.to_string(), amount.to_sat()) + .add_payment( + x_forwarded_for, + Some(&address), + Some(&user), + amount.to_sat(), + ) .await; let resp = { diff --git a/src/payments.rs b/src/payments.rs index b21ac86..db78fb8 100644 --- a/src/payments.rs +++ b/src/payments.rs @@ -1,3 +1,6 @@ +use crate::auth::AuthUser; +use crate::MAX_SEND_AMOUNT; +use bitcoin::Address; use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -57,8 +60,25 @@ impl PaymentsByIp { } } + pub async fn add_payment( + &self, + ip: &str, + address: Option<&Address>, + user: Option<&AuthUser>, + amount: u64, + ) { + self.add_payment_impl(ip, amount).await; + if let Some(address) = address { + self.add_payment_impl(&address.to_string(), amount).await; + } + if let Some(user) = user { + self.add_payment_impl(format!("github:{}", user.username).as_str(), amount) + .await; + } + } + // Add a payment to the tracker for the given ip - pub async fn add_payment(&self, ip: &str, amount: u64) { + async fn add_payment_impl(&self, ip: &str, amount: u64) { let mut trackers = self.trackers.lock().await; let tracker = trackers .entry(ip.to_string()) @@ -74,4 +94,31 @@ impl PaymentsByIp { None => 0, } } + + pub async fn verify_payments( + &self, + ip: &str, + address: Option<&Address>, + user: Option<&AuthUser>, + ) -> bool { + let mut total = 0; + let mut addr_amt = 0; + let mut trackers = self.trackers.lock().await; + if let Some(tracker) = trackers.get_mut(ip) { + total += tracker.sum_payments(); + } + if let Some(address) = address { + if let Some(tracker) = trackers.get_mut(&address.to_string()) { + let amt = tracker.sum_payments(); + total += amt; + addr_amt = amt; + } + }; + if let Some(user) = user { + if let Some(tracker) = trackers.get_mut(format!("github:{}", user.username).as_str()) { + total += tracker.sum_payments(); + } + } + total >= MAX_SEND_AMOUNT * 10 || addr_amt >= MAX_SEND_AMOUNT + } } diff --git a/src/setup.rs b/src/setup.rs index 71a24b9..5b998ac 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -3,6 +3,7 @@ use std::env; use nostr::key::Keys; use tonic_openssl_lnd::lnrpc; +use crate::auth::AuthState; use crate::AppState; pub async fn setup() -> anyhow::Result { @@ -15,6 +16,22 @@ pub async fn setup() -> anyhow::Result { let host = env::var("HOST").expect("missing HOST"); + // Load environment variables + let github_client_id = env::var("GITHUB_CLIENT_ID").expect("GITHUB_CLIENT_ID must be set"); + let github_client_secret = + env::var("GITHUB_CLIENT_SECRET").expect("GITHUB_CLIENT_SECRET must be set"); + let jwt_secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set"); + + if github_client_id.is_empty() { + panic!("GITHUB_CLIENT_ID must be set"); + } + if github_client_secret.is_empty() { + panic!("GITHUB_CLIENT_SECRET must be set"); + } + if jwt_secret.is_empty() { + panic!("JWT_SECRET must be set"); + } + // read keys from env, otherwise generate one let keys = env::var("NSEC") .map(|k| Keys::parse(k).expect("Invalid nsec")) @@ -58,5 +75,12 @@ pub async fn setup() -> anyhow::Result { lightning_client }; - Ok(AppState::new(host, keys, lightning_client, network)) + let auth = AuthState { + client: reqwest::Client::new(), + github_client_id, + github_client_secret, + jwt_secret, + }; + + Ok(AppState::new(host, keys, lightning_client, network, auth)) }