diff options
Diffstat (limited to 'server/src/routes/ui/account/mod.rs')
-rw-r--r-- | server/src/routes/ui/account/mod.rs | 261 |
1 files changed, 0 insertions, 261 deletions
diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs deleted file mode 100644 index 83a1447..0000000 --- a/server/src/routes/ui/account/mod.rs +++ /dev/null @@ -1,261 +0,0 @@ -/* - 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) 2025 metamuffin <metamuffin.org> -*/ -pub mod session; -pub mod settings; - -use super::{ - error::MyError, - layout::{trs, LayoutPage}, -}; -use crate::{ - database::Database, - routes::{ - locale::AcceptLanguage, - ui::{ - account::session::Session, error::MyResult, home::rocket_uri_macro_r_home, - layout::DynLayoutPage, - }, - }, - uri, -}; -use anyhow::anyhow; -use argon2::{password_hash::Salt, Argon2, PasswordHasher}; -use chrono::Duration; -use jellybase::{locale::tr, CONF}; -use jellycommon::user::{User, UserPermission}; -use rocket::{ - form::{Contextual, Form}, - get, - http::{Cookie, CookieJar}, - post, - response::Redirect, - FromForm, State, -}; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; - -#[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(lang: AcceptLanguage) -> DynLayoutPage<'static> { - let AcceptLanguage(lang) = lang; - LayoutPage { - title: tr(lang, "account.register").to_string(), - content: markup::new! { - form.account[method="POST", action=""] { - h1 { @trs(&lang, "account.register") } - - label[for="inp-invitation"] { @trs(&lang, "account.register.invitation") } - input[type="text", id="inp-invitation", name="invitation"]; br; - - label[for="inp-username"] { @trs(&lang, "account.username") } - input[type="text", id="inp-username", name="username"]; br; - label[for="inp-password"] { @trs(&lang, "account.password") } - input[type="password", id="inp-password", name="password"]; br; - - input[type="submit", value=&*tr(lang, "account.register.submit")]; - - p { @trs(&lang, "account.register.login") " " a[href=uri!(r_account_login())] { @trs(&lang, "account.register.login_here") } } - } - }, - ..Default::default() - } -} - -#[derive(FromForm, Serialize, Deserialize)] -pub struct LoginForm { - #[field(validate = len(4..32))] - pub username: String, - #[field(validate = len(..64))] - pub password: String, - #[field(default = 604800)] // one week - pub expire: u64, -} - -#[get("/account/login")] -pub fn r_account_login(sess: Option<Session>, lang: AcceptLanguage) -> DynLayoutPage<'static> { - let AcceptLanguage(lang) = lang; - let logged_in = sess.is_some(); - let title = tr( - lang, - if logged_in { - "account.login.switch" - } else { - "account.login" - }, - ); - LayoutPage { - title: title.to_string(), - content: markup::new! { - form.account[method="POST", action=""] { - h1 { @title.to_string() } - - label[for="inp-username"] { @trs(&lang, "account.username") } - input[type="text", id="inp-username", name="username"]; br; - label[for="inp-password"] { @trs(&lang, "account.password") } - input[type="password", id="inp-password", name="password"]; br; - - input[type="submit", value=&*tr(lang, if logged_in { "account.login.submit.switch" } else { "account.login.submit" })]; - - @if logged_in { - p { @trs(&lang, "account.login.register.switch") " " a[href=uri!(r_account_register())] { @trs(&lang, "account.login.register_here") } } - } else { - p { @trs(&lang, "account.login.cookie_note") } - p { @trs(&lang, "account.login.register") " " a[href=uri!(r_account_register())] { @trs(&lang, "account.login.register_here") } } - } - } - }, - ..Default::default() - } -} - -#[get("/account/logout")] -pub fn r_account_logout(lang: AcceptLanguage) -> DynLayoutPage<'static> { - let AcceptLanguage(lang) = lang; - LayoutPage { - title: tr(lang, "account.logout").to_string(), - content: markup::new! { - form.account[method="POST", action=""] { - h1 { @trs(&lang, "account.logout") } - input[type="submit", value=&*tr(lang, "account.logout.submit")]; - } - }, - ..Default::default() - } -} - -#[post("/account/register", data = "<form>")] -pub fn r_account_register_post<'a>( - database: &'a State<Database>, - _sess: Option<Session>, - form: Form<Contextual<'a, RegisterForm>>, - lang: AcceptLanguage, -) -> MyResult<DynLayoutPage<'a>> { - let AcceptLanguage(lang) = lang; - let logged_in = _sess.is_some(); - let form = match &form.value { - Some(v) => v, - None => return Err(format_form_error(form)), - }; - - database.register_user( - &form.invitation, - &form.username, - User { - display_name: form.username.clone(), - name: form.username.clone(), - password: hash_password(&form.username, &form.password), - ..Default::default() - }, - )?; - - Ok(LayoutPage { - title: tr(lang, "account.register.success.title").to_string(), - content: markup::new! { - h1 { @trs(&lang, if logged_in { - "account.register.success.switch" - } else { - "account.register.success" - })} - }, - ..Default::default() - }) -} - -#[post("/account/login", data = "<form>")] -pub fn r_account_login_post( - database: &State<Database>, - jar: &CookieJar, - form: Form<Contextual<LoginForm>>, -) -> MyResult<Redirect> { - let form = match &form.value { - Some(v) => v, - None => return Err(format_form_error(form)), - }; - jar.add( - Cookie::build(( - "session", - login_logic(database, &form.username, &form.password, None, None)?, - )) - .permanent() - .build(), - ); - - Ok(Redirect::found(rocket::uri!(r_home()))) -} - -#[post("/account/logout")] -pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Redirect> { - jar.remove_private(Cookie::build("session")); - Ok(Redirect::found(rocket::uri!(r_home()))) -} - -pub fn login_logic( - database: &Database, - username: &str, - password: &str, - expire: Option<i64>, - drop_permissions: Option<HashSet<UserPermission>>, -) -> MyResult<String> { - // hashing the password regardless if the accounts exists to better resist timing attacks - let password = hash_password(username, password); - - let mut user = database - .get_user(username)? - .ok_or(anyhow!("invalid password"))?; - - if user.password != password { - Err(anyhow!("invalid password"))? - } - - if let Some(ep) = drop_permissions { - // remove all grant perms that are in `ep` - user.permissions - .0 - .retain(|p, val| if *val { !ep.contains(p) } else { true }) - } - - Ok(session::token::create( - user.name, - user.permissions, - Duration::days(CONF.login_expire.min(expire.unwrap_or(i64::MAX))), - )) -} - -pub fn format_form_error<T>(form: Form<Contextual<T>>) -> 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("<unknown>".to_string()) - ) - } - MyError(anyhow!(k)) -} - -pub fn hash_password(username: &str, password: &str) -> Vec<u8> { - Argon2::default() - .hash_password( - format!("{username}\0{password}").as_bytes(), - <&str as TryInto<Salt>>::try_into("IYMa13osbNeLJKnQ1T8LlA").unwrap(), - ) - .unwrap() - .hash - .unwrap() - .as_bytes() - .to_vec() -} |