working on the global page fairing state

This commit is contained in:
Artemis 2025-01-25 16:08:37 +01:00
parent 2aeb49954d
commit d8acbcb7fa
5 changed files with 119 additions and 19 deletions

View file

@ -30,6 +30,32 @@ pub fn logout(cookies: &CookieJar<'_>) {
#[derive(Debug)]
pub struct SessionInternalFailure();
/// A struct that checks the presence of a session in cookie
///
/// This guard can be used to know the probable session status,
/// e.g. for rendering pages (to determine if a login button may be displayed).
///
/// In no way this guard should be trusted for an actual authentication check as
/// it does not verify the authenticity of the session against the user database.
/// For authentication, use the [`User`] guard instead.
///
/// The Uuid is the currently logged-in account's UUID.
#[derive(Debug)]
pub struct Session(pub Option<Uuid>);
#[rocket::async_trait]
impl<'a> FromRequest<'a> for Session {
type Error = ();
async fn from_request(req: &'a Request<'_>) -> Outcome<Self, Self::Error> {
let cookies = req.cookies();
Outcome::Success(Session(check_login(&cookies)))
}
}
/// Guard that checks that the user has an active session cookie and,
/// using the UUID from this cookie checks in the DB if the user is enabled
#[rocket::async_trait]
impl<'a> FromRequest<'a> for User {
type Error = SessionInternalFailure;
@ -47,7 +73,13 @@ impl<'a> FromRequest<'a> for User {
error!("User::from_request internal error: {:?}", err);
Outcome::Error((Status::InternalServerError, SessionInternalFailure()))
}
Ok(Some(user)) => Outcome::Success(user),
Ok(Some(user)) => {
if user.enabled {
Outcome::Success(user)
} else {
Outcome::Forward(Status::Unauthorized)
}
}
Ok(None) => Outcome::Forward(Status::Unauthorized),
}
} else {

View file

@ -68,6 +68,14 @@ pub struct CreateDollProfile<'a> {
pub chassis_color: Option<&'a str>,
}
/// A user
///
/// This type implements [`FromRequest`][rocket::request::FromRequest],
/// using it as a guard is the safe way to verify that the user is logged in
/// and that they can be logged in.
///
/// If you only need to check the login status without any security impact
/// (e.g. for page template rendering reasons), use [`Session`][crate::auth::session::Session] instead.
#[derive(Debug)]
pub struct User {
pub id: Uuid,

View file

@ -1,35 +1,25 @@
#[macro_use]
extern crate rocket;
use std::collections::HashMap;
use auth::session;
use db::migrate::run_migrations;
use db::schema::{DollTags, User};
use db::schema::DollTags;
use rocket::fairing::AdHoc;
use rocket::fs::{relative, FileServer};
use rocket_db_pools::Database;
use rocket_dyn_templates::tera::try_get_value;
use rocket_dyn_templates::Template;
use routes::form::accounts;
use routes::{error_handlers, form, public};
use serde_json::{to_value, Value};
pub mod auth;
pub mod db;
pub mod ids;
pub mod pages;
pub mod routes;
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(Template::custom(|engines| {
engines
.tera
.register_filter("pretty_id", |v: &Value, _: &HashMap<String, Value>| {
let value = try_get_value!("pretty_id", "value", i32, v);
Ok(to_value(ids::id_db_to_public(value)).unwrap())
});
}))
.attach(pages::init_templates())
.attach(DollTags::init())
.attach(AdHoc::try_on_ignite("SQLx migrations", run_migrations))
.register(

46
src/pages.rs Normal file
View file

@ -0,0 +1,46 @@
use std::collections::HashMap;
use rocket::{
fairing::Fairing,
outcome::try_outcome,
request::{FromRequest, Outcome},
Request,
};
use rocket_dyn_templates::{tera::try_get_value, Template};
use serde_json::{to_value, Value};
use crate::{auth::session::Session, ids};
pub fn init_templates() -> impl Fairing {
Template::custom(|engines| {
engines
.tera
.register_filter("pretty_id", |v: &Value, _: &HashMap<String, Value>| {
let value = try_get_value!("pretty_id", "value", i32, v);
Ok(to_value(ids::id_db_to_public(value)).unwrap())
});
})
}
/// A bundle of all the state all pages may make use of.
///
/// Implements [`FromRequest`] to provide easy access to it as a guard.
/// This guard never fails.
#[derive(Debug)]
pub struct CommonTemplateState {
/// true if the user is logged in (doesn't check the DB, instead uses [`Session`])
pub logged_in: bool,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for CommonTemplateState {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<CommonTemplateState, ()> {
let session_state = try_outcome!(request.guard::<Session>().await);
Outcome::Success(CommonTemplateState {
logged_in: session_state.0.is_some(),
})
}
}

View file

@ -9,7 +9,10 @@ use rocket_dyn_templates::{context, Template};
use crate::{
auth::{pw, session},
db::{schema::DollTagsDb, user},
db::{
schema::{DollTagsDb, User},
user,
},
routes::error_handlers::{PageResponse, PageResult},
};
@ -24,8 +27,13 @@ pub struct AuthForm<'a> {
// needed cuz the var. name changes how the query string is parsed
#[allow(unused_variables)]
#[get("/login?<next>")]
pub fn show_login(next: Option<&str>) -> Template {
Template::render("account/login", context! {})
pub fn show_login(next: Option<&str>, maybe_loggedin: Option<User>) -> PageResult {
if maybe_loggedin.is_some() {
let next = String::from(next.unwrap_or("/"));
Ok(Redirect::to(next).into())
} else {
Ok(Template::render("account/login", context! {}).into())
}
}
#[post("/login?<next>", data = "<form>")]
@ -34,7 +42,13 @@ pub async fn handle_login(
next: Option<&str>,
form: Form<Contextual<'_, AuthForm<'_>>>,
cookies: &CookieJar<'_>,
maybe_loggedin: Option<User>,
) -> PageResult {
if maybe_loggedin.is_some() {
let next = String::from(next.unwrap_or("/"));
return Ok(Redirect::to(next).into());
}
let miss = || PageResponse::Page(Template::render("account/login", context! {failure: true}));
let values = match &form.value {
@ -62,13 +76,18 @@ pub async fn handle_login(
}
#[get("/register")]
pub fn show_register() -> Template {
Template::render(
pub fn show_register(maybe_loggedin: Option<User>) -> PageResult {
if maybe_loggedin.is_some() {
return Ok(Redirect::to("/").into());
}
Ok(Template::render(
"account/register",
context! {
previous: form::Context::default(),
},
)
.into())
}
#[derive(Debug, FromForm)]
@ -104,7 +123,12 @@ pub async fn handle_register(
db: DollTagsDb,
form: Form<Contextual<'_, RegisterForm<'_>>>,
cookies: &CookieJar<'_>,
maybe_loggedin: Option<User>,
) -> PageResult {
if maybe_loggedin.is_some() {
return Ok(Redirect::to("/").into());
}
let values = match form.value {
Some(ref v) => v,
None => {