use std::ops::RangeInclusive; use axum::{ extract::{Form, Path, State}, response::IntoResponse, }; use sqlx::SqlitePool; use unicode_segmentation::UnicodeSegmentation; use super::{util, CreateUserError, CreateUserErrorKind, SignupForm}; use crate::{ templates::*, user::{ForgejoUser, User}, }; const PASSWORD_STRENGTH: f64 = 50.0; const USERNAME_LEN: RangeInclusive = 1..=50; const DISPLAYNAME_LEN: RangeInclusive = 0..=100; const EMAIL_LEN: RangeInclusive = 4..=50; const CHECKOUT_TIMEOUT: i64 = 12 * 3600; lazy_static! { static ref ADMIN_TOKEN: String = std::env::var("ADMIN_TOKEN").unwrap(); static ref FORGEJO_URL: String = std::env::var("FORGEJO_URL").unwrap(); static ref STRIPE_TOKEN: String = std::env::var("STRIPE_TOKEN").unwrap(); static ref ANNUAL_LINK: String = std::env::var("ANNUAL_LINK").unwrap(); static ref MONTHLY_LINK: String = std::env::var("MONTHLY_LINK").unwrap(); } /// Displays the signup form. pub async fn get_signup(_db: State) -> impl IntoResponse { SignupPage { monthly_link: Some((*MONTHLY_LINK).to_string()), ..Default::default() } } /// Receives the form with the user signup fields filled out. pub async fn post_signup( db: State, Form(form): Form, ) -> Result { let user = validate_signup(&form).await?; if create_user(&user) { log::info!("Created user {user:?}"); Ok(SignupSuccessPage(user)) } else { Err(CreateUserError(CreateUserErrorKind::UnknownEorr)) } } /// Redirected from Stripe with the receipt of payment. pub async fn payment_success( db: State, receipt: Option>, ) -> impl IntoResponse { let receipt = if let Some(Path(receipt)) = receipt { receipt } else { return CreateUserError(CreateUserErrorKind::BadPayment).into_response(); }; UserFormPage { receipt, ..Default::default() } .into_response() } //-************************************************************************ // helpers //-************************************************************************ fn create_user(user: &User) -> bool { let token = &*ADMIN_TOKEN; let url = &*FORGEJO_URL; let auth_header = format!("token {token}"); let user: ForgejoUser = user.into(); let resp = ureq::post(&format!("{url}/api/v1/admin/users")) .set("Authorization", &auth_header) .set("Content-Type", "application/json") .set("accept", "application/json") .send_json(user); match resp { Ok(resp) => resp.status() == 201, Err(resp) => { log::error!("Got error from user creation request: {}", resp); false } } } fn confirm_payment(stripe_checkout_session_id: &str) -> bool { let token = &*STRIPE_TOKEN; let url = format!("https://api.stripe.com/v1/checkout/sessions/{stripe_checkout_session_id}"); let json: serde_json::Value = ureq::get(&url) .set("Authorization", &format!("Bearer {token}")) .call() .map_err(|e| { log::error!("Error confirming payment from Stripe, got {e}"); std::io::Error::new(std::io::ErrorKind::Other, e) }) .and_then(|resp| resp.into_json()) .unwrap_or_default(); // see https://docs.stripe.com/api/checkout/sessions/retrieve let total = json["amount_subtotal"].as_i64().unwrap_or(0); let created_at = json["created"].as_i64().unwrap_or(0); let now = chrono::Utc::now(); let then = chrono::DateTime::from_timestamp(created_at, 0).unwrap(); // safe to unwrap let dur = now - then; let max_elapsed = chrono::TimeDelta::new(CHECKOUT_TIMEOUT, 0).unwrap(); (dur < max_elapsed) && (total >= 300) } async fn validate_signup(form: &SignupForm) -> Result { use passwords::{analyzer::analyze, scorer::score}; let username = form.username.trim(); let password = form.password.trim(); let verify = form.pw_verify.trim(); let receipt = form.receipt.trim(); if confirm_payment(receipt) { log::info!("Confirmed payment from {receipt}"); } else { return Err(CreateUserError(CreateUserErrorKind::BadPayment)); } let name_len = username.graphemes(true).size_hint().1.unwrap_or(0); // we are not ascii exclusivists around here if !USERNAME_LEN.contains(&name_len) { return Err(CreateUserErrorKind::BadUsername.into()); } if password != verify { return Err(CreateUserErrorKind::PasswordMismatch.into()); } let strength = score(&analyze(password)); if strength < PASSWORD_STRENGTH { return Err(CreateUserErrorKind::BadPassword.into()); } // clean up the optionals let displayname = util::validate_optional_length( &form.displayname, DISPLAYNAME_LEN, CreateUserErrorKind::BadDisplayname, )?; let email = util::validate_length(&form.email, EMAIL_LEN, CreateUserErrorKind::BadEmail)?; let user = User { username: username.to_string(), displayname, email, password: password.to_string(), pw_verify: verify.to_string(), }; Ok(user) }