/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin */ pub mod admin; pub mod session; use super::error::MyError; use super::layout::LayoutPage; use crate::database::Database; use crate::database::User; use crate::routes::ui::error::MyResult; use crate::routes::ui::home::rocket_uri_macro_r_home; use crate::routes::ui::layout::DynLayoutPage; use crate::CONF; use anyhow::anyhow; use argon2::{Argon2, PasswordHasher}; use rocket::form::Contextual; use rocket::form::Form; use rocket::http::{Cookie, CookieJar}; use rocket::response::Redirect; use rocket::{get, post, uri, FromForm, State}; #[derive(FromForm)] pub struct RegisterForm { #[field(validate = len(8..128))] pub invitation: String, #[field(validate = len(4..32))] pub username: String, #[field(validate = len(4..64))] pub password: String, } #[get("/account/register")] pub async fn r_account_register() -> DynLayoutPage<'static> { LayoutPage { title: "Register".to_string(), content: markup::new! { form.account[method="POST", action=""] { h1 { "Register for " @CONF.brand } label[for="inp-invitation"] { "Invite Code: " } input[type="text", id="inp-invitation", name="invitation"]; br; label[for="inp-username"] { "Username: " } input[type="text", id="inp-username", name="username"]; br; label[for="inp-password"] { "Password: " } input[type="password", id="inp-password", name="password"]; br; input[type="submit", value="Register now!"]; } }, } } #[derive(FromForm)] pub struct LoginForm { #[field(validate = len(4..32))] pub username: String, #[field(validate = len(..64))] pub password: String, } #[get("/account/login")] pub fn r_account_login() -> DynLayoutPage<'static> { LayoutPage { title: "Log in".to_string(), content: markup::new! { form.account[method="POST", action=""] { h1 { "Log in to your Account" } label[for="inp-username"] { "Username: " } input[type="text", id="inp-username", name="username"]; br; label[for="inp-password"] { "Password: " } input[type="password", id="inp-password", name="password"]; br; input[type="submit", value="Login"]; p { "While logged in, a cookie will be used to identify you." } } }, } } #[get("/account/logout")] pub fn r_account_logout() -> DynLayoutPage<'static> { LayoutPage { title: "Log out".to_string(), content: markup::new! { form.account[method="POST", action=""] { h1 { "Log out" } input[type="submit", value="Log out."]; } }, } } #[post("/account/register", data = "
")] pub fn r_account_register_post<'a>( database: &'a State, form: Form>, ) -> MyResult> { let form = match &form.value { Some(v) => v, None => return Err(format_form_error(form)), }; if database.invites.remove(&form.invitation).unwrap().is_none() { return Err(MyError(anyhow!("invitation invalid"))); } match database .users .compare_and_swap( &form.username, None, Some(&User { display_name: form.username.clone(), name: form.username.clone(), password: hash_password(&form.password), admin: false, }), ) .unwrap() { Ok(_) => Ok(LayoutPage { title: "Registration successful".to_string(), content: markup::new! { h1 { "Registration successful, you may log in now." } }, }), Err(_) => Err(MyError(anyhow!("username is taken"))), } } #[post("/account/login", data = "")] pub fn r_account_login_post( database: &State, jar: &CookieJar, form: Form>, ) -> MyResult { let form = match &form.value { Some(v) => v, None => return Err(format_form_error(form)), }; // hashing the password regardless if the accounts exists to prevent timing attacks let password = hash_password(&form.password); let user = database .users .get(&form.username)? .ok_or(anyhow!("invalid password"))?; if user.password != password { Err(anyhow!("invalid password"))? } jar.add_private(Cookie::build("user", user.name).permanent().finish()); Ok(Redirect::found(uri!(r_home()))) } #[post("/account/logout")] pub fn r_account_logout_post(jar: &CookieJar) -> MyResult { jar.remove_private(Cookie::named("user")); Ok(Redirect::found(uri!(r_home()))) } fn format_form_error(form: Form>) -> MyError { let mut k = String::from("form validation failed:"); for e in form.context.errors() { k += &format!( "\n\t{}: {e}", e.name .as_ref() .map(|e| e.to_string()) .unwrap_or("".to_string()) ) } MyError(anyhow!(k)) } pub fn hash_password(s: &str) -> Vec { Argon2::default() .hash_password(s.as_bytes(), r"IYMa13osbNeLJKnQ1T8LlA") .unwrap() .hash .unwrap() .as_bytes() .to_vec() }