add logout feature

This commit is contained in:
Jackzie 2025-04-16 14:34:44 -05:00
parent 2c6dfa0797
commit 21e02257c2
6 changed files with 69 additions and 34 deletions

View file

@ -6,7 +6,7 @@ use crate::models::user::UserModel;
use crate::{LoginSessionData, SessionData}; use crate::{LoginSessionData, SessionData};
pub struct AuthUser { pub struct AuthUser {
pub user: LoginSessionData pub session: LoginSessionData
} }
#[derive(Debug)] #[derive(Debug)]
@ -29,7 +29,7 @@ impl<'r> FromRequest<'r> for AuthUser {
_ => return Outcome::Forward(Status::Unauthorized), _ => return Outcome::Forward(Status::Unauthorized),
}; };
if let Some(login) = &sess.login { if let Some(login) = &sess.login {
Outcome::Success(Self { user: login.clone() }) Outcome::Success(Self { session: login.clone() })
} else { } else {
Outcome::Forward(Status::Unauthorized) Outcome::Forward(Status::Unauthorized)
} }

View file

@ -7,6 +7,7 @@ use rocket::data::ByteUnit;
use rocket::fs::{relative, FileServer}; use rocket::fs::{relative, FileServer};
use rocket::futures::AsyncWriteExt; use rocket::futures::AsyncWriteExt;
use rocket::http::private::cookie::CookieBuilder; use rocket::http::private::cookie::CookieBuilder;
use rocket::http::uri::Uri;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::serde::Serialize; use rocket::serde::Serialize;
use rocket_dyn_templates::handlebars::{handlebars_helper, Context, Handlebars, Helper, HelperResult, Output, RenderContext}; use rocket_dyn_templates::handlebars::{handlebars_helper, Context, Handlebars, Helper, HelperResult, Output, RenderContext};
@ -123,7 +124,7 @@ async fn rocket() -> _ {
api::library::move_file, api::library::upload_file, api::library::download_file, api::library::list_files, api::library::get_file, api::library::delete_file, 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![ .mount("/auth", routes![
ui::auth::login, ui::auth::login_handler, ui::auth::register, ui::auth::register_handler, ui::auth::logout, ui::auth::login, ui::auth::login_handler, ui::auth::register, ui::auth::register_handler,
]) ])
.mount("/", routes![ .mount("/", routes![
ui::help::about, ui::help::about,
@ -141,7 +142,7 @@ async fn rocket() -> _ {
#[catch(401)] #[catch(401)]
pub fn not_authorized(req: &Request) -> Redirect { pub fn not_authorized(req: &Request) -> Redirect {
// uri!(ui::auth::login) doesn't work, it redirects to /login instead // uri!(ui::auth::login) doesn't work, it redirects to /login instead
Redirect::to(format!("/auth/login?path={}", req.uri())) Redirect::to(format!("/auth/login?return_to={}", req.uri().path().percent_encode()))
} }
#[catch(404)] #[catch(404)]

View file

@ -1,25 +1,37 @@
use std::net::IpAddr; use std::net::IpAddr;
use log::debug; use log::debug;
use rocket::{get, post, uri, FromForm, Route, State}; use rocket::{get, post, uri, FromForm, Responder, Route, State};
use rocket::form::{Context, Contextual, Error, Form}; use rocket::form::{Context, Contextual, Error, Form};
use rocket::form::error::Entity; use rocket::form::error::Entity;
use rocket::http::Status; use rocket::fs::relative;
use rocket::http::{Header, Status};
use rocket::http::uri::{Origin, Reference, Uri};
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket_dyn_templates::{context, Template}; use rocket_dyn_templates::{context, Template};
use rocket_session_store::Session; use rocket_session_store::Session;
use crate::models::user::{validate_user, validate_user_form, UserAuthError, UserModel}; use crate::models::user::{validate_user, validate_user_form, UserAuthError, UserModel};
use crate::{LoginSessionData, SessionData, DB}; use crate::{LoginSessionData, SessionData, DB};
use crate::guards::AuthUser;
use crate::routes::ui; use crate::routes::ui;
use crate::routes::ui::user::list_library_files; use crate::routes::ui::user::list_library_files;
use crate::util::{gen_csrf_token, set_csrf, validate_csrf_form, JsonErrorResponse, ResponseError}; use crate::util::{gen_csrf_token, set_csrf, validate_csrf_form, JsonErrorResponse, ResponseError};
#[get("/login")] #[get("/logout")]
pub async fn login(route: &Route, session: Session<'_, SessionData>) -> Template { pub async fn logout(session: Session<'_, SessionData>, user: AuthUser) -> Redirect {
session.remove().await.unwrap();
Redirect::to(uri!("/auth", login(_, 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; let csrf_token = set_csrf(&session).await;
Template::render("auth/login", context! { Template::render("auth/login", context! {
route: route.uri.path(), route: route.uri.path(),
csrf_token: csrf_token, csrf_token: csrf_token,
form: &Context::default(), form: &Context::default(),
return_to,
logged_out
}) })
} }
@ -36,15 +48,22 @@ struct LoginForm<'r> {
} }
#[derive(Responder)]
#[response(status = 302)]
struct HackyRedirectBecauseRocketBug {
inner: String,
location: Header<'static>,
}
#[post("/login", data = "<form>")] #[post("/login?<return_to>", data = "<form>")]
pub async fn login_handler( pub async fn login_handler(
pool: &State<DB>, pool: &State<DB>,
route: &Route, route: &Route,
ip_addr: IpAddr, ip_addr: IpAddr,
session: Session<'_, SessionData>, session: Session<'_, SessionData>,
mut form: Form<Contextual<'_, LoginForm<'_>>>, mut form: Form<Contextual<'_, LoginForm<'_>>>,
) -> Result<Redirect, Template> { return_to: Option<String>,
) -> Result<HackyRedirectBecauseRocketBug, Template> {
validate_csrf_form(&mut form.context, &session).await; validate_csrf_form(&mut form.context, &session).await;
let user = validate_user_form(&mut form.context, &pool).await; let user = validate_user_form(&mut form.context, &pool).await;
if form.context.status() == Status::Ok { if form.context.status() == Status::Ok {
@ -56,15 +75,23 @@ pub async fn login_handler(
ip_address: ip_addr, ip_address: ip_addr,
}), }),
}).await.unwrap(); }).await.unwrap();
debug!("returning user to {:?}", return_to);
return Ok(Redirect::to(uri!(ui::user::index()))) 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 csrf_token = set_csrf(&session).await;
let ctx = context! { let ctx = context! {
csrf_token, csrf_token,
form: &form.context form: &form.context,
return_to
}; };
Err(Template::render("auth/login", &ctx)) Err(Template::render("auth/login", &ctx))
} }

View file

@ -20,12 +20,12 @@ use crate::util::{JsonErrorResponse, ResponseError};
#[get("/")] #[get("/")]
pub async fn index(route: &Route) -> Template { pub async fn index(user: AuthUser, route: &Route) -> Template {
Template::render("index", context! { user: true, route: route.uri.path(), test: "value" }) Template::render("index", context! { session: user.session, route: route.uri.path(), test: "value" })
} }
#[get("/library/<library_id>")] #[get("/library/<library_id>")]
pub async fn redirect_list_library_files(libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str) pub async fn redirect_list_library_files(user: AuthUser, libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str)
-> Result<Redirect, ResponseError> -> Result<Redirect, ResponseError>
{ {
let libs = libraries.lock().await; let libs = libraries.lock().await;
@ -67,7 +67,7 @@ pub async fn list_library_files(user: AuthUser, route: &Route, libraries: &State
debug!("parent={:?}", parent); debug!("parent={:?}", parent);
debug!("segments={:?}", segments); debug!("segments={:?}", segments);
Ok(Template::render("libraries", context! { Ok(Template::render("libraries", context! {
user: user.user, session: user.session,
route: route.uri.path(), route: route.uri.path(),
library: library.model(), library: library.model(),
files: files, files: files,
@ -92,7 +92,7 @@ struct FileAttachment {
} }
#[get("/file/<library_id>/<path..>")] #[get("/file/<library_id>/<path..>")]
pub async fn get_library_file<'a>(libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: PathBuf) pub async fn get_library_file<'a>(user: AuthUser, libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: PathBuf)
-> Result<FileAttachment, ResponseError> -> Result<FileAttachment, ResponseError>
{ {
let libs = libraries.lock().await; let libs = libraries.lock().await;

View file

@ -4,7 +4,7 @@
<h1 class="title is-1 has-text-centered">storage-app</h1> <h1 class="title is-1 has-text-centered">storage-app</h1>
<div class="box is-radiusless"> <div class="box is-radiusless">
<h4 class="title is-4 has-text-centered">Login</h4> <h4 class="title is-4 has-text-centered">Login</h4>
{{#unless (eq (len form.form_errors) 0) }} {{#unless (eq (len form.form_errors) 0) }}
<div class="notification is-danger is-light"> <div class="notification is-danger is-light">
<b>Login failed with errors:</b> <b>Login failed with errors:</b>
<ul> <ul>
@ -14,7 +14,12 @@
</ul> </ul>
</div> </div>
{{/unless}} {{/unless}}
<form method="post" action="/auth/login"> {{#if logged_out }}
<div class="notification is-success is-light">
You have been logged out successfully.
</div>
{{/if}}
<form method="post" action="/auth/login?return_to={{return_to}}">
<input type="hidden" name="_csrf" value="{{ csrf_token }}"> <input type="hidden" name="_csrf" value="{{ csrf_token }}">
<div class="field"> <div class="field">
<label class="label">Username / Email</label> <label class="label">Username / Email</label>

View file

@ -14,7 +14,7 @@
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
{{#if user}} <!-- TODO: only show w/ route is library/files --> {{#if session.user }} <!-- TODO: only show w/ route is library/files -->
<div class="navbar-item" style="width:300px"> <div class="navbar-item" style="width:300px">
<div class="field" style="width:100%" > <div class="field" style="width:100%" >
<p class="control has-icons-left"> <p class="control has-icons-left">
@ -26,7 +26,7 @@
</div> </div>
</div> </div>
{{/if}} {{/if}}
{{#if user}} {{#if session.user }}
<div class="navbar-item"> <div class="navbar-item">
<a class="icon has-text-black"> <a class="icon has-text-black">
<i class="far fa-bell"></i> <i class="far fa-bell"></i>
@ -37,25 +37,27 @@
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> <a class="navbar-link">
<img src="/static/img/default_user.png" alt="User Image" /> <img src="/static/img/default_user.png" alt="User Image" />
{{ debug user }} {{ session.user.name }}
</a> </a>
<div class="navbar-dropdown"> <div class="navbar-dropdown mr-4 is-right is-radiusless">
<div class="dropdown-content"> <div class="dropdown-content">
<div class="block px-4 py-4"> <div class="navbar-item">
<b>Quota</b> {{ session.user.username }}
<progress class="progress" min=0 value=20 max=100 />
</div> </div>
<div class="navbar-item">
<a class="navbar-item" href="/help/about"> {{ session.user.email }}
About </div>
<hr class="navbar-divider">
<a class="navbar-item" href="/settings">
<i class="fa fa-cog"></i>Settings
</a> </a>
<a class="navbar-item"> <a class="navbar-item" href="/admin">
Contact <i class="fa fa-star"></i> Admin Panel
</a> </a>
<hr class="navbar-divider"> <hr class="navbar-divider">
<a class="navbar-item"> <a class="navbar-item has-text-danger" href="/auth/logout">
Report an issue <i class="fa fa-square-up-right"></i>Logout
</a> </a>
</div> </div>