diff options
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(()), + } + }) + } +} |