diff options
| author | metamuffin <metamuffin@disroot.org> | 2023-01-22 13:56:06 +0100 | 
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2023-01-22 13:56:06 +0100 | 
| commit | ec76bbe5398f51ffa55bfd315b30c0a07245d4e6 (patch) | |
| tree | fa0e1723f861de6fee21a35524bb7768fab0b6ce /server/src/routes/ui/account | |
| parent | 84e093afa908dc68a7b0ae97ba8dc76aa0901d26 (diff) | |
| download | jellything-ec76bbe5398f51ffa55bfd315b30c0a07245d4e6.tar jellything-ec76bbe5398f51ffa55bfd315b30c0a07245d4e6.tar.bz2 jellything-ec76bbe5398f51ffa55bfd315b30c0a07245d4e6.tar.zst  | |
this is *horrible*
Diffstat (limited to 'server/src/routes/ui/account')
| -rw-r--r-- | server/src/routes/ui/account/mod.rs | 148 | ||||
| -rw-r--r-- | server/src/routes/ui/account/session.rs | 58 | 
2 files changed, 178 insertions, 28 deletions
diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs index 7e329a1..74710d9 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/routes/ui/account/mod.rs @@ -1,8 +1,20 @@ -use super::HtmlTemplate; +pub mod session; + +use super::error::MyError; +use super::layout::LayoutPage; +use crate::database::Database;  use crate::database::User; -use crate::{AppState, CONF}; +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::{get, post, FromForm, State}; +use rocket::http::{Cookie, CookieJar}; +use rocket::response::Redirect; +use rocket::{get, post, uri, FromForm, State};  #[derive(FromForm)]  pub struct RegisterForm { @@ -15,10 +27,10 @@ pub struct RegisterForm {  }  #[get("/account/register")] -pub fn r_account_register() -> HtmlTemplate<markup::DynRender<'static>> { -    HtmlTemplate( -        "Register".to_string(), -        markup::new! { +pub async fn r_account_register() -> DynLayoutPage<'static> { +    LayoutPage { +        title: "Register".to_string(), +        content: markup::new! {              h1 { "Register for " @CONF.brand  }              form[method="POST", action=""] {                  label[for="inp-invitation"] { "Invite Code: " } @@ -32,41 +44,121 @@ pub fn r_account_register() -> HtmlTemplate<markup::DynRender<'static>> {                  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() -> HtmlTemplate<markup::DynRender<'static>> { -    HtmlTemplate( -        "Log in".to_string(), -        markup::new! { +pub fn r_account_login() -> DynLayoutPage<'static> { +    LayoutPage { +        title: "Log in".to_string(), +        content: markup::new! {              h1 { "Log in to your Account" } +            form[method="POST", action=""] { +                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." }          }, -    ) +    }  }  #[post("/account/register", data = "<form>")] -pub fn r_account_register_post( -    state: &State<AppState>, -    form: Form<RegisterForm>, -) -> HtmlTemplate<markup::DynRender<'static>> { -    state -        .database +pub fn r_account_register_post<'a>( +    database: &'a State<Database>, +    form: Form<Contextual<'a, RegisterForm>>, +) -> MyResult<DynLayoutPage<'a>> { +    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 -        .insert( +        .compare_and_swap(              &form.username, -            &User { +            None, +            Some(&User {                  display_name: form.username.clone(),                  name: form.username.clone(),                  password: form.password.clone().into(), // TODO hash it +                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 = "<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)), +    }; + +    // 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()))) +} + +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())          ) -        .unwrap(); -    HtmlTemplate( -        "Registration successful".to_string(), -        markup::new! { -            h1 { "Registration successful." } -        }, -    ) +    } +    MyError(anyhow!(k)) +} + +pub fn hash_password(s: &str) -> Vec<u8> { +    Argon2::default() +        .hash_password(s.as_bytes(), r"IYMa13osbNeLJKnQ1T8LlA") +        .unwrap() +        .hash +        .unwrap() +        .as_bytes() +        .to_vec()  } diff --git a/server/src/routes/ui/account/session.rs b/server/src/routes/ui/account/session.rs new file mode 100644 index 0000000..9c50099 --- /dev/null +++ b/server/src/routes/ui/account/session.rs @@ -0,0 +1,58 @@ +use crate::{ +    database::{Database, User}, +    routes::ui::error::MyError, +}; +use anyhow::anyhow; +use rocket::{ +    outcome::Outcome, +    request::{self, FromRequest}, +    Request, State, +}; + +pub struct Session { +    pub user: User, +} + +impl Session { +    pub async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> { +        let cookie = req +            .cookies() +            .get_private("user") +            .ok_or(anyhow!("login required"))?; +        let username = cookie.value(); + +        let db = req.guard::<&State<Database>>().await.unwrap(); +        let user = db +            .users +            .get(&username.to_string())? +            .ok_or(anyhow!("user not found"))?; + +        Ok(Session { user }) +    } +} + +impl<'r> FromRequest<'r> for Session { +    type Error = MyError; + +    fn from_request<'life0, 'async_trait>( +        request: &'r Request<'life0>, +    ) -> core::pin::Pin< +        Box< +            dyn core::future::Future<Output = request::Outcome<Self, Self::Error>> +                + core::marker::Send +                + 'async_trait, +        >, +    > +    where +        'r: 'async_trait, +        'life0: 'async_trait, +        Self: 'async_trait, +    { +        Box::pin(async move { +            match Self::from_request_ut(request).await { +                Ok(x) => Outcome::Success(x), +                Err(_) => Outcome::Forward(()), +            } +        }) +    } +}  |