queen/src/handlers/handlers.rs

163 lines
5.2 KiB
Rust

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<usize> = 1..=50;
const DISPLAYNAME_LEN: RangeInclusive<usize> = 0..=100;
const EMAIL_LEN: RangeInclusive<usize> = 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<SqlitePool>) -> 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<SqlitePool>,
Form(form): Form<SignupForm>,
) -> Result<impl IntoResponse, CreateUserError> {
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<SqlitePool>,
receipt: Option<Path<String>>,
) -> 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<User, CreateUserError> {
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)
}