mirror of
https://github.com/Jackzmc/storage.git
synced 2025-05-05 21:43:21 +00:00
Add reset password form, cleanup
This commit is contained in:
parent
1bdb4b6dc7
commit
4f755cfeca
10 changed files with 258 additions and 103 deletions
2
server/Cargo.lock
generated
2
server/Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
|
||||
|
|
|
@ -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?<return_to>&<logged_out>")]
|
||||
pub async fn login(route: &Route, session: Session<'_, SessionData>, return_to: Option<String>, logged_out: Option<bool>) -> 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?<return_to>", data = "<form>")]
|
||||
pub async fn login_handler(
|
||||
pool: &State<DB>,
|
||||
route: &Route,
|
||||
ip_addr: IpAddr,
|
||||
session: Session<'_, SessionData>,
|
||||
mut form: Form<Contextual<'_, LoginForm<'_>>>,
|
||||
return_to: Option<String>,
|
||||
) -> Result<HackyRedirectBecauseRocketBug, Template> {
|
||||
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::<Origin>(&return_to_path).unwrap_or(Uri::parse::<Origin>("/").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() })
|
||||
|
||||
}
|
39
server/src/routes/ui/auth/forgot_password.rs
Normal file
39
server/src/routes/ui/auth/forgot_password.rs
Normal file
|
@ -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?<return_to>")]
|
||||
pub async fn page(
|
||||
route: &Route,
|
||||
session: Session<'_, SessionData>,
|
||||
meta: &State<GlobalMetadata>,
|
||||
return_to: Option<String>,
|
||||
) -> 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?<return_to>", data = "<form>")]
|
||||
pub async fn handler(form: Form<Contextual<'_, ForgotPasswordForm<'_>>>, return_to: Option<String>) -> Template {
|
||||
todo!()
|
||||
}
|
94
server/src/routes/ui/auth/login.rs
Normal file
94
server/src/routes/ui/auth/login.rs
Normal file
|
@ -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?<return_to>&<logged_out>")]
|
||||
pub async fn page(
|
||||
route: &Route,
|
||||
session: Session<'_, SessionData>,
|
||||
meta: &State<GlobalMetadata>,
|
||||
return_to: Option<String>,
|
||||
logged_out: Option<bool>
|
||||
) -> 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?<return_to>", data = "<form>")]
|
||||
pub async fn handler(
|
||||
pool: &State<DB>,
|
||||
route: &Route,
|
||||
ip_addr: IpAddr,
|
||||
session: Session<'_, SessionData>,
|
||||
meta: &State<GlobalMetadata>,
|
||||
mut form: Form<Contextual<'_, LoginForm<'_>>>,
|
||||
return_to: Option<String>,
|
||||
) -> Result<HackyRedirectBecauseRocketBug, Template> {
|
||||
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::<Origin>(&return_to_path).unwrap_or(Uri::parse::<Origin>("/").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))
|
||||
}
|
21
server/src/routes/ui/auth/register.rs
Normal file
21
server/src/routes/ui/auth/register.rs
Normal file
|
@ -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<GlobalMetadata>) -> 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() })
|
||||
|
||||
}
|
67
server/templates/auth/forgot-password.html.hbs
Normal file
67
server/templates/auth/forgot-password.html.hbs
Normal file
|
@ -0,0 +1,67 @@
|
|||
{{#> layouts/default body-class="has-background-white-ter login-bg" }}
|
||||
<br><br>
|
||||
<div class="container py-6" style="width:20%"> <!-- TODO: fix width on mobile -->
|
||||
<h1 class="title is-1 has-text-centered">{{ meta.app_name }}</h1>
|
||||
<div class="box is-radiusless">
|
||||
<h4 class="title is-4 has-text-centered">Forgot Password</h4>
|
||||
<p class="subtitle is-6 mt-2 has-text-centered">An email will be sent to reset your password</p>
|
||||
|
||||
{{#if email_available }}
|
||||
{{#unless (eq (len form.form_errors) 0) }}
|
||||
<div class="notification is-danger is-light">
|
||||
<b>Failed with errors:</b>
|
||||
<ul>
|
||||
{{#each form.form_errors}}
|
||||
<li>{{msg}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if success }}
|
||||
<div class="notification is-success is-light">
|
||||
An email has been sent if an account exists with that email address.
|
||||
</div>
|
||||
{{/if}}
|
||||
<form method="post" action="/auth/login?return_to={{return_to}}">
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<div class="field">
|
||||
<div class="control has-icons-left">
|
||||
<input required name="email" class="input {{#if errors.email}}is-danger{{/if}}" type="email" placeholder="Email">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
</div>
|
||||
{{#if errors.email }}
|
||||
<p class="help is-danger">{{errors.email}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="buttons">
|
||||
<button class="button is-link is-fullwidth" type="submit" >Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="notification is-warning is-light">
|
||||
Email support is unavailable, please contact an administrator to reset your password.
|
||||
</div>
|
||||
{{/if}}
|
||||
<br>
|
||||
<span>
|
||||
<a href="/auth/login">Login</a>
|
||||
{{#if can_register}}
|
||||
| <a href="/auth/register">Register</a>{{/if}}
|
||||
</span>
|
||||
<div class="field is-pulled-right">
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select>
|
||||
<option selected value="en-us">English</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Powered by <b><a href="{{meta.repo_url}}">{{ meta.app_name }}</a></b> v{{meta.app_version}}</p>
|
||||
</div>
|
||||
{{/layouts/default}}
|
|
@ -1,7 +1,7 @@
|
|||
{{#> layouts/default body-class="has-background-white-ter login-bg" }}
|
||||
<br><br>
|
||||
<div class="container py-6" style="width:20%"> <!-- TODO: fix width on mobile -->
|
||||
<h1 class="title is-1 has-text-centered">storage-app</h1>
|
||||
<h1 class="title is-1 has-text-centered">{{ meta.app_name }}</h1>
|
||||
<div class="box is-radiusless">
|
||||
<h4 class="title is-4 has-text-centered">Login</h4>
|
||||
{{#unless (eq (len form.form_errors) 0) }}
|
||||
|
@ -45,14 +45,15 @@
|
|||
<p class="help is-danger">{{errors.password}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="field">
|
||||
{{!-- TODO: Not implemented --}}
|
||||
{{!-- <div class="field">
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input name="remember_me" type="checkbox">
|
||||
Remember Me</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div> --}}
|
||||
<hr>
|
||||
<div class="buttons">
|
||||
<button class="button is-link is-fullwidth" type="submit" >Login</button>
|
||||
|
@ -77,5 +78,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Powered by <b><a href="{{meta.repo_url}}">{{ meta.app_name }}</a></b> v{{meta.app_version}}</p>
|
||||
</div>
|
||||
{{/layouts/default}}
|
|
@ -1,7 +1,7 @@
|
|||
{{#> layouts/default body-class="has-background-white-ter login-bg" }}
|
||||
<br><br>
|
||||
<div class="container py-6" style="width:20%"> <!-- TODO: fix width on mobile -->
|
||||
<h1 class="title is-1 has-text-centered">storage-app</h1>
|
||||
<h1 class="title is-1 has-text-centered">{{ meta.app_name }}</h1>
|
||||
<div class="box is-radiusless">
|
||||
<h4 class="title is-4 has-text-centered">Register</h4>
|
||||
{{#if can_register }}
|
||||
|
@ -53,10 +53,8 @@
|
|||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="block ml-2 content">
|
||||
<p><i class="fas fa-xmark"></i>Registeration has been disabled</p>
|
||||
|
||||
{{!-- <p>Contact administrator</p> --}}
|
||||
<div class="notification is-danger is-light">
|
||||
<p><i class="fas fa-xmark"></i> Registration has been disabled</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
<span>
|
||||
|
@ -72,5 +70,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Powered by <b><a href="{{meta.repo_url}}">{{ meta.app_name }}</a></b> v{{meta.app_version}}</p>
|
||||
</div>
|
||||
{{/layouts/default}}
|
Loading…
Add table
Add a link
Reference in a new issue