diff --git a/Cargo.lock b/Cargo.lock index 7a0688b..3bedd41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "num-traits", +] + [[package]] name = "cookie" version = "0.18.0" @@ -607,6 +616,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -721,6 +739,7 @@ dependencies = [ "askama", "askama_axum", "axum", + "chrono", "dotenvy", "env_logger", "justerror", @@ -728,6 +747,7 @@ dependencies = [ "log", "rand", "serde", + "serde_json", "thiserror", "time", "tokio", @@ -785,12 +805,58 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustls" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -877,6 +943,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -1201,6 +1279,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "ureq" version = "2.9.6" @@ -1210,7 +1294,13 @@ dependencies = [ "base64", "log", "once_cell", + "rustls", + "rustls-pki-types", + "rustls-webpki", + "serde", + "serde_json", "url", + "webpki-roots", ] [[package]] @@ -1236,6 +1326,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1367,3 +1466,9 @@ name = "windows_x86_64_msvc" version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index 77b8fea..77f8042 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" askama = { version = "0.12", default-features = false, features = ["with-axum", "serde"] } askama_axum = { version = "0.4", default-features = false } axum = { version = "0.7", default-features = false, features = ["tokio", "http1", "form"] } +chrono = { version = "0.4", default-features = false, features = ["now"] } dotenvy = { version = "0.15", default-features = false } env_logger = { version = "0.11", default-features = false, features = ["humantime"] } justerror = { version = "1" } @@ -14,10 +15,11 @@ lazy_static = "1" log = { version = "0.4", default-features = false } rand = { version = "0.8", default-features = false, features = ["getrandom"] } serde = { version = "1", default-features = false, features = ["derive"] } +serde_json = { version = "1", default-features = false } thiserror = { version = "1" } time = { version = "0.3", default-features = false } tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] } tower-http = { version = "0.5", default-features = false, features = ["fs"] } tower-sessions = { version = "0.10", default-features = false, features = ["axum-core", "memory-store"] } unicode-segmentation = { version = "1", default-features = false } -ureq = { version = "2", default-features = false } +ureq = { version = "2", default-features = false, features = ["json", "tls"] } diff --git a/src/handlers.rs b/src/handlers.rs deleted file mode 100644 index f3b708c..0000000 --- a/src/handlers.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::{error::Error, fmt::Debug, ops::RangeInclusive}; - -use axum::{ - extract::{Form, Path}, - http::StatusCode, - response::{IntoResponse, Redirect, Response}, -}; -use rand::random; -use serde::Deserialize; -use tower_sessions::Session; -use unicode_segmentation::UnicodeSegmentation; - -use crate::{templates::*, User}; - -const PASSWORD_LEN: RangeInclusive = 4..=100; -const USERNAME_LEN: RangeInclusive = 1..=50; -const DISPLAYNAME_LEN: RangeInclusive = 0..=100; -const EMAIL_LEN: RangeInclusive = 4..=50; - -lazy_static! { - static ref SIGNUP_KEY: String = format!("meow-{}", random::()); -} - -#[Error(desc = "Could not create user.")] -#[non_exhaustive] -pub struct CreateUserError(#[from] pub CreateUserErrorKind); - -impl IntoResponse for CreateUserError { - fn into_response(self) -> Response { - let mut resp = SignupErrorPage(format!("{self}")).into_response(); - *resp.status_mut() = StatusCode::FORBIDDEN; - resp - } -} - -#[Error] -#[non_exhaustive] -pub enum CreateUserErrorKind { - #[error(desc = "That username already exists")] - AlreadyExists, - #[error(desc = "Usernames must be between 1 and 50 characters long")] - BadUsername, - #[error(desc = "Your passwords didn't match")] - PasswordMismatch, - #[error(desc = "Password must have at least 4 and at most 100 characters")] - BadPassword, - #[error(desc = "Display name must be less than 100 characters long")] - BadDisplayname, - #[error(desc = "Your email is too short, it simply can't be real")] - BadEmail, - #[error(desc = "We could not verify your payment")] - BadPayment, - #[error(desc = "We couldn't retrieve your info from this browser session")] - NoFormFound, -} - -#[derive(Debug, Default, Deserialize, PartialEq, Eq)] -pub struct SignupForm { - pub username: String, - #[serde(default, deserialize_with = "empty_string_as_none")] - pub displayname: Option, - pub email: String, - pub password: String, - pub pw_verify: String, -} - -/// Displays the signup form. -pub async fn get_signup() -> impl IntoResponse { - SignupPage { - ..Default::default() - } -} - -/// Receives the form with the user signup fields filled out. -pub async fn post_signup( - session: Session, - Form(form): Form, -) -> Result { - let user = validate_signup(&form).await?; - session.insert(&SIGNUP_KEY, user).await.unwrap(); - - Ok(Redirect::to( - "https://buy.stripe.com/test_eVa6rrb7ygjNbwk000", - )) -} - -/* -/// Handles the case when there was an error in the form -pub async fn get_edit_signup( - session: Session, - receipt: Option>, -) -> Result { - Ok(()) -} -*/ - -/// Redirected from Stripe with the receipt of payment. -pub async fn payment_success(session: Session, receipt: Option>) -> impl IntoResponse { - let user: User = session.get(&SIGNUP_KEY).await.unwrap().unwrap_or_default(); - - if receipt.is_none() { - log::info!("Got {:?} from the session, but no receipt.", &user); - return CreateUserError(CreateUserErrorKind::BadPayment).into_response(); - } - let Path(receipt) = receipt.unwrap(); - - if user == User::default() { - log::warn!("Could not find user in session; got receipt {}", receipt); - return CreateUserError(CreateUserErrorKind::NoFormFound).into_response(); - } - - // TODO: call the forgejo admin api to create the user - - session.delete().await.unwrap_or_else(|e| { - log::error!("Got error deleting {} from session, got {}", &user, e); - }); - - log::info!("Added {:?}", &user); - SignupSuccessPage(user).into_response() -} - -//-************************************************************************ -// helpers -//-************************************************************************ -async fn validate_signup(form: &SignupForm) -> Result { - let username = form.username.trim(); - let password = form.password.trim(); - let verify = form.pw_verify.trim(); - - let name_len = username.graphemes(true).size_hint().1.unwrap(); - // 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 pwlen = password.graphemes(true).size_hint().1.unwrap_or(0); - if !PASSWORD_LEN.contains(&pwlen) { - return Err(CreateUserErrorKind::BadPassword.into()); - } - - // clean up the optionals - let displayname = validate_optional_length( - &form.displayname, - DISPLAYNAME_LEN, - CreateUserErrorKind::BadDisplayname, - )?; - - let email = 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) -} - -pub(crate) fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, - T: std::str::FromStr, - T::Err: std::fmt::Display, -{ - let opt = as serde::Deserialize>::deserialize(de)?; - match opt.as_deref() { - None | Some("") => Ok(None), - Some(s) => std::str::FromStr::from_str(s) - .map_err(serde::de::Error::custom) - .map(Some), - } -} - -pub(crate) fn validate_optional_length( - opt: &Option, - len_range: RangeInclusive, - err: E, -) -> Result, E> { - if let Some(opt) = opt { - let opt = opt.trim(); - let len = opt.graphemes(true).size_hint().1.unwrap(); - if !len_range.contains(&len) { - Err(err) - } else { - Ok(Some(opt.to_string())) - } - } else { - Ok(None) - } -} - -pub(crate) fn validate_length( - thing: &str, - len_range: RangeInclusive, - err: E, -) -> Result { - let thing = thing.trim(); - let len = thing.graphemes(true).size_hint().1.unwrap(); - if !len_range.contains(&len) { - Err(err) - } else { - Ok(thing.to_string()) - } -} diff --git a/src/handlers/handlers.rs b/src/handlers/handlers.rs new file mode 100644 index 0000000..ed3548f --- /dev/null +++ b/src/handlers/handlers.rs @@ -0,0 +1,153 @@ +use std::ops::RangeInclusive; + +use axum::{ + extract::{Form, Path}, + response::{IntoResponse, Redirect}, +}; +use rand::random; +use tower_sessions::Session; +use unicode_segmentation::UnicodeSegmentation; + +use super::{util, CreateUserError, CreateUserErrorKind, SignupForm}; +use crate::{ + templates::*, + user::{ForgejoUser, User}, +}; + +const PASSWORD_LEN: RangeInclusive = 4..=100; +const USERNAME_LEN: RangeInclusive = 1..=50; +const DISPLAYNAME_LEN: RangeInclusive = 0..=100; +const EMAIL_LEN: RangeInclusive = 4..=50; + +lazy_static! { + static ref SIGNUP_KEY: String = format!("meow-{}", random::()); +} + +/// Displays the signup form. +pub async fn get_signup() -> impl IntoResponse { + SignupPage { + ..Default::default() + } +} + +/// Receives the form with the user signup fields filled out. +pub async fn post_signup( + session: Session, + Form(form): Form, +) -> Result { + let user = validate_signup(&form).await?; + session.insert(&SIGNUP_KEY, user).await.unwrap(); + + Ok(Redirect::to( + "https://buy.stripe.com/test_eVa6rrb7ygjNbwk000", + )) +} + +/// Redirected from Stripe with the receipt of payment. +pub async fn payment_success(session: Session, receipt: Option>) -> impl IntoResponse { + let user: User = session.get(&SIGNUP_KEY).await.unwrap().unwrap_or_default(); + + if receipt.is_none() { + log::info!("Got {:?} from the session, but no receipt.", &user); + return CreateUserError(CreateUserErrorKind::BadPayment).into_response(); + } + let Path(receipt) = receipt.unwrap(); + + if user == User::default() { + log::warn!("Could not find user in session; got receipt {}", receipt); + return CreateUserError(CreateUserErrorKind::NoFormFound).into_response(); + } + + if !confirm_payment(&receipt) { + return CreateUserError(CreateUserErrorKind::BadPayment).into_response(); + } + + if !create_user(&user) { + return CreateUserError(CreateUserErrorKind::AlreadyExists).into_response(); + } + // TODO: store the receipt into a durable store to prevent re-use after creating + // an account + + session.delete().await.unwrap_or_else(|e| { + log::error!("Got error deleting {} from session, got {}", &user, e); + }); + + log::info!("Added {:?}", &user); + SignupSuccessPage(user).into_response() +} + +//-************************************************************************ +// helpers +//-************************************************************************ +fn create_user(user: &User) -> bool { + let token = std::env::var("ADMIN_TOKEN").expect("Could not find $ADMIN_TOKEN in environment."); + let url = std::env::var("ADD_USER_ENDPOINT") + .expect("Could not find $ADD_USER_ENDPOINT in environment"); + let auth_header = format!("token {token}"); + let user: ForgejoUser = user.into(); + let resp = ureq::post(&format!("http://{url}/api/v1/admin/users")) + .set("Authorization", &auth_header) + .send_json(user) + .unwrap(); + resp.status() == 201 +} + +fn confirm_payment(stripe_checkout_session_id: &str) -> bool { + let token = std::env::var("STRIPE_TOKEN").expect("Could not find $STRIPE_TOKEN in environment"); + 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| 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_total"].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(); + let dur = now - then; + let max_elapsed = chrono::TimeDelta::new(12 * 3600, 0).unwrap(); + + (dur < max_elapsed) && (total > 0) +} + +async fn validate_signup(form: &SignupForm) -> Result { + let username = form.username.trim(); + let password = form.password.trim(); + let verify = form.pw_verify.trim(); + + let name_len = username.graphemes(true).size_hint().1.unwrap(); + // 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 pwlen = password.graphemes(true).size_hint().1.unwrap_or(0); + if !PASSWORD_LEN.contains(&pwlen) { + 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) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..a4cc91c --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,56 @@ +use std::fmt::Debug; + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde::Deserialize; + +use crate::templates::*; + +pub mod handlers; +mod util; + +#[Error(desc = "Could not create user.")] +#[non_exhaustive] +pub struct CreateUserError(#[from] pub CreateUserErrorKind); + +impl IntoResponse for CreateUserError { + fn into_response(self) -> Response { + let kind = self; + let mut resp = SignupErrorPage(format!("{kind}")).into_response(); + *resp.status_mut() = StatusCode::FORBIDDEN; + resp + } +} + +#[Error] +#[non_exhaustive] +pub enum CreateUserErrorKind { + #[error(desc = "That username already exists")] + AlreadyExists, + #[error(desc = "Usernames must be between 1 and 50 characters long")] + BadUsername, + #[error(desc = "Your passwords didn't match")] + PasswordMismatch, + #[error(desc = "Password must have at least 4 and at most 100 characters")] + BadPassword, + #[error(desc = "Display name must be less than 100 characters long")] + BadDisplayname, + #[error(desc = "Your email is too short, it simply can't be real")] + BadEmail, + #[error(desc = "We could not verify your payment")] + BadPayment, + #[error(desc = "We couldn't retrieve your info from this browser session")] + NoFormFound, +} + +#[derive(Debug, Default, Deserialize, PartialEq, Eq)] +pub struct SignupForm { + pub username: String, + #[serde(default, deserialize_with = "util::empty_string_as_none")] + pub displayname: Option, + pub email: String, + pub password: String, + pub pw_verify: String, +} diff --git a/src/handlers/util.rs b/src/handlers/util.rs new file mode 100644 index 0000000..6ce5c1c --- /dev/null +++ b/src/handlers/util.rs @@ -0,0 +1,50 @@ +use std::{error::Error, ops::RangeInclusive}; + +use unicode_segmentation::UnicodeSegmentation; + +pub(crate) fn empty_string_as_none<'de, D, T>(de: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: std::str::FromStr, + T::Err: std::fmt::Display, +{ + let opt = as serde::Deserialize>::deserialize(de)?; + match opt.as_deref() { + None | Some("") => Ok(None), + Some(s) => std::str::FromStr::from_str(s) + .map_err(serde::de::Error::custom) + .map(Some), + } +} + +pub(crate) fn validate_optional_length( + opt: &Option, + len_range: RangeInclusive, + err: E, +) -> Result, E> { + if let Some(opt) = opt { + let opt = opt.trim(); + let len = opt.graphemes(true).size_hint().1.unwrap(); + if !len_range.contains(&len) { + Err(err) + } else { + Ok(Some(opt.to_string())) + } + } else { + Ok(None) + } +} + +pub(crate) fn validate_length( + thing: &str, + len_range: RangeInclusive, + err: E, +) -> Result { + let thing = thing.trim(); + let len = thing.graphemes(true).size_hint().1.unwrap(); + if !len_range.contains(&len) { + Err(err) + } else { + Ok(thing.to_string()) + } +} diff --git a/src/main.rs b/src/main.rs index 80252ae..fb09c29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,14 @@ -use std::{io::Write, net::SocketAddr}; +use std::{ + env::VarError, + io::Write, + net::{Ipv4Addr, SocketAddr}, +}; use axum::{ routing::{get, MethodRouter}, Router, }; +use tokio::net::TcpListener; use tower_http::services::ServeDir; use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer}; @@ -14,16 +19,14 @@ extern crate justerror; extern crate lazy_static; mod handlers; -use handlers::{get_signup, payment_success, post_signup}; - mod templates; - mod user; -use user::User; #[tokio::main] async fn main() { + use handlers::handlers::{get_signup, payment_success, post_signup}; init(); + // for javascript and css let assets_dir = std::env::current_dir().unwrap().join("assets"); let assets_svc = ServeDir::new(assets_dir.as_path()); @@ -32,7 +35,7 @@ async fn main() { let session_store = MemoryStore::default(); let session_layer = SessionManagerLayer::new(session_store) .with_secure(true) - .with_expiry(Expiry::OnInactivity(time::Duration::hours(16))); + .with_expiry(Expiry::OnInactivity(time::Duration::hours(2))); // the core application, defining the routes and handlers let app = Router::new() @@ -42,24 +45,38 @@ async fn main() { .route("/payment_success/:receipt", get(payment_success)) .layer(session_layer) .into_make_service(); - - // listening on the network - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + let listener = mklistener().await; axum::serve(listener, app).await.unwrap(); } +//-************************************************************************ +// li'l helpers +//-************************************************************************ fn init() { dotenvy::dotenv().expect("Could not read .env file."); env_logger::builder() .format(|buf, record| { - // let ts = buf.timestamp(); writeln!(buf, "{}: {}", ts, record.args()) }) .init(); } +async fn mklistener() -> TcpListener { + let ip = + std::env::var("LISTENING_ADDR").expect("Could not find $LISTENING_ADDR in environment"); + let ip: Ipv4Addr = ip + .parse() + .unwrap_or_else(|_| panic!("Could not parse {ip} as an IP address")); + let port: u16 = std::env::var("LISTENING_PORT") + .and_then(|p| p.parse().map_err(|_| VarError::NotPresent)) + .unwrap_or_else(|_| { + panic!("Could not find LISTENING_PORT in env or parse if present"); + }); + let addr = SocketAddr::from((ip, port)); + TcpListener::bind(&addr).await.unwrap() +} + /// Adds both routes, with and without a trailing slash. trait RouterPathStrip where diff --git a/src/templates.rs b/src/templates.rs index f77dad0..c830250 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,7 +1,7 @@ use askama::Template; use serde::{Deserialize, Serialize}; -use crate::User; +use crate::user::User; #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq)] #[template(path = "signup.html")] diff --git a/src/user.rs b/src/user.rs index 908d9f0..0455ea2 100644 --- a/src/user.rs +++ b/src/user.rs @@ -2,7 +2,7 @@ use std::fmt::{Debug, Display}; use serde::{Deserialize, Serialize}; -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Default)] pub struct User { pub username: String, pub displayname: Option, @@ -43,3 +43,24 @@ impl Display for User { write!(f, "Username: {uname}\nDisplayname: {dname}\nEmail: {email}") } } + +#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Default)] +pub struct ForgejoUser<'u> { + pub username: &'u str, + pub full_name: Option<&'u str>, + pub email: &'u str, + pub password: &'u str, + pub must_change_password: bool, +} + +impl<'u> From<&'u User> for ForgejoUser<'u> { + fn from(user: &'u User) -> Self { + Self { + username: &user.username, + full_name: user.displayname.as_deref(), + email: &user.email, + password: &user.password, + must_change_password: false, + } + } +}