163 lines
5.2 KiB
Rust
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)
|
|
}
|