From 4f755cfecaa70bb8145de87e69865788e717f485 Mon Sep 17 00:00:00 2001 From: Jackz Date: Wed, 16 Apr 2025 15:51:42 -0500 Subject: [PATCH] Add reset password form, cleanup --- server/Cargo.lock | 2 +- server/Cargo.toml | 4 +- server/src/main.rs | 21 +++- server/src/routes/ui/auth.rs | 96 ++----------------- server/src/routes/ui/auth/forgot_password.rs | 39 ++++++++ server/src/routes/ui/auth/login.rs | 94 ++++++++++++++++++ server/src/routes/ui/auth/register.rs | 21 ++++ .../templates/auth/forgot-password.html.hbs | 67 +++++++++++++ server/templates/auth/login.html.hbs | 8 +- server/templates/auth/register.html.hbs | 9 +- 10 files changed, 258 insertions(+), 103 deletions(-) create mode 100644 server/src/routes/ui/auth/forgot_password.rs create mode 100644 server/src/routes/ui/auth/login.rs create mode 100644 server/src/routes/ui/auth/register.rs create mode 100644 server/templates/auth/forgot-password.html.hbs diff --git a/server/Cargo.lock b/server/Cargo.lock index 0af3fb1..4e3684d 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -2356,7 +2356,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "storage-server" +name = "storage-app" version = "0.1.0" dependencies = [ "anyhow", diff --git a/server/Cargo.toml b/server/Cargo.toml index f45a242..9d4e4c9 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,7 +1,9 @@ [package] -name = "storage-server" +name = "storage-app" version = "0.1.0" edition = "2024" +repository = "https://github.com/jackzmc/storage" + [dependencies] rocket = { version = "0.5.1", features = ["json", "uuid"] } diff --git a/server/src/main.rs b/server/src/main.rs index f167d0f..5c09848 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -59,6 +59,12 @@ struct SessionUser { name: String, email: String } +#[derive(Serialize, Clone)] +pub struct GlobalMetadata { + app_name: String, + app_version: String, + repo_url: String, +} #[launch] async fn rocket() -> _ { setup_logger(); @@ -103,11 +109,18 @@ async fn rocket() -> _ { // slash which prevents the cookie from being sent for `example.com/myapp2/`). .path("/") }; + + let metadata = GlobalMetadata { + app_name: env!("CARGO_PKG_NAME").to_string(), + app_version: env!("CARGO_PKG_VERSION").to_string(), + repo_url: env!("CARGO_PKG_REPOSITORY").to_string(), + }; rocket::build() .manage(pool) .manage(repo_manager) .manage(libraries_manager) + .manage(metadata) .attach(store.fairing()) .attach(Template::custom(|engines| { @@ -123,8 +136,10 @@ async fn rocket() -> _ { .mount("/api/library", routes![ api::library::move_file, api::library::upload_file, api::library::download_file, api::library::list_files, api::library::get_file, api::library::delete_file, ]) - .mount("/auth", routes![ - ui::auth::logout, ui::auth::login, ui::auth::login_handler, ui::auth::register, ui::auth::register_handler, + .mount("/", routes![ + ui::auth::logout, + ui::auth::login::page, ui::auth::login::handler, ui::auth::register::page, ui::auth::register::handler, + ui::auth::forgot_password::page, ui::auth::forgot_password::handler, ]) .mount("/", routes![ ui::help::about, @@ -141,7 +156,7 @@ async fn rocket() -> _ { #[catch(401)] pub fn not_authorized(req: &Request) -> Redirect { - // uri!(ui::auth::login) doesn't work, it redirects to /login instead + // TODO: do uri!() Redirect::to(format!("/auth/login?return_to={}", req.uri().path().percent_encode())) } diff --git a/server/src/routes/ui/auth.rs b/server/src/routes/ui/auth.rs index cdd5dd0..6404591 100644 --- a/server/src/routes/ui/auth.rs +++ b/server/src/routes/ui/auth.rs @@ -10,103 +10,19 @@ use rocket::response::Redirect; use rocket_dyn_templates::{context, Template}; use rocket_session_store::Session; use crate::models::user::{validate_user, validate_user_form, UserAuthError, UserModel}; -use crate::{LoginSessionData, SessionData, DB}; +use crate::{GlobalMetadata, LoginSessionData, SessionData, DB}; use crate::guards::AuthUser; use crate::routes::ui; use crate::routes::ui::user::list_library_files; use crate::util::{gen_csrf_token, set_csrf, validate_csrf_form, JsonErrorResponse, ResponseError}; +pub mod forgot_password; +pub mod login; +pub mod register; + #[get("/logout")] pub async fn logout(session: Session<'_, SessionData>, user: AuthUser) -> Redirect { session.remove().await.unwrap(); - Redirect::to(uri!("/auth", login(_, Some(true)))) + Redirect::to(uri!(login::page(_, Some(true)))) } -#[get("/login?&")] -pub async fn login(route: &Route, session: Session<'_, SessionData>, return_to: Option, logged_out: Option) -> Template { - // TODO: redirect if already logged in - let csrf_token = set_csrf(&session).await; - Template::render("auth/login", context! { - route: route.uri.path(), - csrf_token: csrf_token, - form: &Context::default(), - return_to, - logged_out - }) -} - -#[derive(FromForm)] -#[derive(Debug)] -struct LoginForm<'r> { - _csrf: &'r str, - #[field(validate = len(1..))] - username: &'r str, - #[field(validate = len(1..))] - password: &'r str, - #[field(default = false)] - remember_me: bool -} - - -#[derive(Responder)] -#[response(status = 302)] -struct HackyRedirectBecauseRocketBug { - inner: String, - location: Header<'static>, -} - -#[post("/login?", data = "
")] -pub async fn login_handler( - pool: &State, - route: &Route, - ip_addr: IpAddr, - session: Session<'_, SessionData>, - mut form: Form>>, - return_to: Option, -) -> Result { - validate_csrf_form(&mut form.context, &session).await; - let user = validate_user_form(&mut form.context, &pool).await; - if form.context.status() == Status::Ok { - if let Some(submission) = &form.value { - session.set(SessionData { - csrf_token: None, - login: Some(LoginSessionData { - user: user.expect("failed to acquire user but no errors"), // if validate_user_form returned None, form had errors, this shouldnt run, - ip_address: ip_addr, - }), - }).await.unwrap(); - debug!("returning user to {:?}", return_to); - let return_to_path = return_to.unwrap_or("/".to_string()); - // Rocket redirect fails when `Redirect::to("/path/ has spaces")` has spaces, so manually do location... works better - return Ok(HackyRedirectBecauseRocketBug { - inner: "Login successful, redirecting...".to_string(), - location: Header::new("Location", return_to_path), - }) - // let return_to_uri = Uri::parse::(&return_to_path).unwrap_or(Uri::parse::("/").unwrap()); - // return Ok(Redirect::found(return_to_uri)) - } - } - - let csrf_token = set_csrf(&session).await; - let ctx = context! { - csrf_token, - form: &form.context, - return_to - }; - Err(Template::render("auth/login", &ctx)) -} - -#[get("/register")] -pub async fn register(route: &Route, session: Session<'_, SessionData>) -> Template { - let csrf_token = set_csrf(&session).await; - Template::render("auth/register", context! { - route: route.uri.path(), - csrf_token: csrf_token - }) -} - -#[post("/register")] -pub async fn register_handler(route: &Route, session: Session<'_, SessionData>) -> Template { - Template::render("auth/register", context! { route: route.uri.path() }) - -} \ No newline at end of file diff --git a/server/src/routes/ui/auth/forgot_password.rs b/server/src/routes/ui/auth/forgot_password.rs new file mode 100644 index 0000000..81b3b3d --- /dev/null +++ b/server/src/routes/ui/auth/forgot_password.rs @@ -0,0 +1,39 @@ +use rocket::{get, post, FromForm, Route, State}; +use rocket::form::{Context, Contextual, Form}; +use rocket_dyn_templates::{context, Template}; +use rocket_session_store::Session; +use crate::{GlobalMetadata, SessionData}; +use crate::util::set_csrf; + +#[get("/auth/forgot-password?")] +pub async fn page( + route: &Route, + session: Session<'_, SessionData>, + meta: &State, + return_to: Option, +) -> Template { + // TODO: redirect if already logged in + let csrf_token = set_csrf(&session).await; + Template::render("auth/forgot-password", context! { + route: route.uri.path(), + csrf_token: csrf_token, + form: &Context::default(), + return_to, + meta: meta.inner() + }) +} + +#[derive(FromForm)] +#[derive(Debug)] +struct ForgotPasswordForm<'r> { + _csrf: &'r str, + #[field(validate = len(3..))] + #[field(validate = contains('@').or_else(msg!("invalid email address")))] + email: &'r str, +} + + +#[post("/auth/forgot-password?", data = "")] +pub async fn handler(form: Form>>, return_to: Option) -> Template { + todo!() +} \ No newline at end of file diff --git a/server/src/routes/ui/auth/login.rs b/server/src/routes/ui/auth/login.rs new file mode 100644 index 0000000..91e81f0 --- /dev/null +++ b/server/src/routes/ui/auth/login.rs @@ -0,0 +1,94 @@ +use std::net::IpAddr; +use log::debug; +use rocket::{get, post, FromForm, Responder, Route, State}; +use rocket::form::{Context, Contextual, Form}; +use rocket::http::{Header, Status}; +use rocket_dyn_templates::{context, Template}; +use rocket_session_store::Session; +use crate::{GlobalMetadata, LoginSessionData, SessionData, DB}; +use crate::models::user::validate_user_form; +use crate::util::{set_csrf, validate_csrf_form}; + +#[get("/auth/login?&")] +pub async fn page( + route: &Route, + session: Session<'_, SessionData>, + meta: &State, + return_to: Option, + logged_out: Option +) -> Template { + // TODO: redirect if already logged in + let csrf_token = set_csrf(&session).await; + Template::render("auth/login", context! { + route: route.uri.path(), + csrf_token: csrf_token, + form: &Context::default(), + return_to, + logged_out, + meta: meta.inner() + }) +} + +#[derive(FromForm)] +#[derive(Debug)] +struct LoginForm<'r> { + _csrf: &'r str, + #[field(validate = len(1..))] + username: &'r str, + #[field(validate = len(1..))] + password: &'r str, + #[field(default = false)] + remember_me: bool +} + + +#[derive(Responder)] +#[response(status = 302)] +struct HackyRedirectBecauseRocketBug { + inner: String, + location: Header<'static>, +} + +#[post("/auth/login?", data = "")] +pub async fn handler( + pool: &State, + route: &Route, + ip_addr: IpAddr, + session: Session<'_, SessionData>, + meta: &State, + mut form: Form>>, + return_to: Option, +) -> Result { + validate_csrf_form(&mut form.context, &session).await; + let user = validate_user_form(&mut form.context, &pool).await; + if form.context.status() == Status::Ok { + if let Some(submission) = &form.value { + session.set(SessionData { + csrf_token: None, + login: Some(LoginSessionData { + user: user.expect("failed to acquire user but no errors"), // if validate_user_form returned None, form had errors, this shouldnt run, + ip_address: ip_addr, + }), + }).await.unwrap(); + debug!("returning user to {:?}", return_to); + let return_to_path = return_to.unwrap_or("/".to_string()); + // Rocket redirect fails when `Redirect::to("/path/ has spaces")` has spaces, so manually do location... works better + return Ok(HackyRedirectBecauseRocketBug { + inner: "Login successful, redirecting...".to_string(), + location: Header::new("Location", return_to_path), + }) + // let return_to_uri = Uri::parse::(&return_to_path).unwrap_or(Uri::parse::("/").unwrap()); + // return Ok(Redirect::found(return_to_uri)) + } + } + + let csrf_token = set_csrf(&session).await; + let ctx = context! { + route: route.uri.path(), + csrf_token: csrf_token, + form: &Context::default(), + return_to, + meta: meta.inner() + }; + Err(Template::render("auth/login", &ctx)) +} \ No newline at end of file diff --git a/server/src/routes/ui/auth/register.rs b/server/src/routes/ui/auth/register.rs new file mode 100644 index 0000000..f870441 --- /dev/null +++ b/server/src/routes/ui/auth/register.rs @@ -0,0 +1,21 @@ +use rocket::{get, post, Route, State}; +use rocket_dyn_templates::{context, Template}; +use rocket_session_store::Session; +use crate::{GlobalMetadata, SessionData}; +use crate::util::set_csrf; + +#[get("/auth/register")] +pub async fn page(route: &Route, session: Session<'_, SessionData>, meta: &State) -> Template { + let csrf_token = set_csrf(&session).await; + Template::render("auth/register", context! { + route: route.uri.path(), + csrf_token: csrf_token, + meta: meta.inner() + }) +} + +#[post("/auth/register")] +pub async fn handler(route: &Route, session: Session<'_, SessionData>) -> Template { + Template::render("auth/register", context! { route: route.uri.path() }) + +} \ No newline at end of file diff --git a/server/templates/auth/forgot-password.html.hbs b/server/templates/auth/forgot-password.html.hbs new file mode 100644 index 0000000..07b7d94 --- /dev/null +++ b/server/templates/auth/forgot-password.html.hbs @@ -0,0 +1,67 @@ +{{#> layouts/default body-class="has-background-white-ter login-bg" }} +

+
+

{{ meta.app_name }}

+
+

Forgot Password

+

An email will be sent to reset your password

+ + {{#if email_available }} + {{#unless (eq (len form.form_errors) 0) }} +
+ Failed with errors: +
    + {{#each form.form_errors}} +
  • {{msg}}
  • + {{/each}} +
+
+ {{/unless}} + + {{#if success }} +
+ An email has been sent if an account exists with that email address. +
+ {{/if}} + + +
+
+ + + + +
+ {{#if errors.email }} +

{{errors.email}}

+ {{/if}} +
+
+
+ +
+ + {{else}} +
+ Email support is unavailable, please contact an administrator to reset your password. +
+ {{/if}} +
+ + Login + {{#if can_register}} + | Register{{/if}} + +
+
+
+ +
+
+
+
+

Powered by {{ meta.app_name }} v{{meta.app_version}}

+
+{{/layouts/default}} \ No newline at end of file diff --git a/server/templates/auth/login.html.hbs b/server/templates/auth/login.html.hbs index fe48481..a585cb0 100644 --- a/server/templates/auth/login.html.hbs +++ b/server/templates/auth/login.html.hbs @@ -1,7 +1,7 @@ {{#> layouts/default body-class="has-background-white-ter login-bg" }}

-

storage-app

+

{{ meta.app_name }}

Login

{{#unless (eq (len form.form_errors) 0) }} @@ -45,14 +45,15 @@

{{errors.password}}

{{/if}}
-
+ {{!-- TODO: Not implemented --}} + {{!--
-
+
--}}
@@ -77,5 +78,6 @@
+

Powered by {{ meta.app_name }} v{{meta.app_version}}

{{/layouts/default}} \ No newline at end of file diff --git a/server/templates/auth/register.html.hbs b/server/templates/auth/register.html.hbs index 5b276c1..e857d60 100644 --- a/server/templates/auth/register.html.hbs +++ b/server/templates/auth/register.html.hbs @@ -1,7 +1,7 @@ {{#> layouts/default body-class="has-background-white-ter login-bg" }}

-

storage-app

+

{{ meta.app_name }}

Register

{{#if can_register }} @@ -53,10 +53,8 @@
{{else}} -
-

Registeration has been disabled

- - {{!--

Contact administrator

--}} +
+

Registration has been disabled

{{/if}} @@ -72,5 +70,6 @@
+

Powered by {{ meta.app_name }} v{{meta.app_version}}

{{/layouts/default}} \ No newline at end of file