diff --git a/crates/ezidam/src/menu/items/admin.rs b/crates/ezidam/src/menu/items/admin.rs index 702f9a0..a04cba8 100644 --- a/crates/ezidam/src/menu/items/admin.rs +++ b/crates/ezidam/src/menu/items/admin.rs @@ -24,7 +24,7 @@ impl AdminMenu { MainItem { id: AdminMenu::Exit.id(), label: "Exit admin panel", - link: uri!(routes::root::homepage).to_string(), + link: uri!(routes::root::home::homepage).to_string(), icon: Icon::Logout.svg, sub: None, }, diff --git a/crates/ezidam/src/menu/items/user.rs b/crates/ezidam/src/menu/items/user.rs index f7e3bb5..804e9e9 100644 --- a/crates/ezidam/src/menu/items/user.rs +++ b/crates/ezidam/src/menu/items/user.rs @@ -17,7 +17,7 @@ impl UserMenu { vec![MainItem { id: UserMenu::Home.id(), label: "Home", - link: uri!(routes::root::homepage).to_string(), + link: uri!(routes::root::home::homepage).to_string(), icon: Icon::Home.svg, sub: None, }] diff --git a/crates/ezidam/src/routes/root.rs b/crates/ezidam/src/routes/root.rs index 18199a0..0f55f42 100644 --- a/crates/ezidam/src/routes/root.rs +++ b/crates/ezidam/src/routes/root.rs @@ -1,86 +1,26 @@ use super::prelude::*; -use crate::tokens::{JWT_COOKIE_NAME, REFRESH_TOKEN_COOKIE_NAME}; -use rocket::http::{Cookie, CookieJar}; -use rocket::{get, post}; -use settings::Settings; -use users::User; +use avatar::*; +use home::*; +use logo::*; +use logout::*; + +pub mod avatar; +pub mod home; +pub mod logo; +pub mod logout; pub fn routes() -> Vec { routes![ - logo, - avatar, + get_logo, + user_avatar, homepage, homepage_user, homepage_redirect, redirect_to_setup, - logout, + request_logout, ] } -#[get("/logo")] -async fn logo(mut db: Connection) -> Result { - // Get settings - let settings = Settings::get(&mut *db).await?; - - // HTTP response - Ok(FileFromBytes::from(settings.business_logo())) -} - -#[cfg(test)] -mod test { - use crate::tests::*; - - #[test] - fn logo() { - // Setup http server - let client = setup_rocket_testing(); - - // Make request - let response = client.get(uri!(super::logo)).dispatch(); - assert_eq!(response.status(), Status::Ok); - - // Assert size of logo - let logo_length = response.into_bytes().expect("bytes containing logo").len(); - use settings::DEFAULT_BUSINESS_LOGO; - assert_eq!( - logo_length, - DEFAULT_BUSINESS_LOGO.len(), - "Invalid logo size in bytes, value was `{logo_length}`", - ); - } -} - -#[get("/avatar/?")] -async fn avatar( - mut db: Connection, - _user: JwtUser, - user_id: RocketUserID, - size: Option, -) -> Result { - // Verify existence of user - User::get_by_login(&mut *db, user_id.0.as_ref()) - .await? - .ok_or_else(|| Error::not_found(user_id.0.to_string()))?; - - // Generate avatar - let avatar = task::spawn_blocking(move || { - let mut avatar = identicon_rs::Identicon::new(user_id.0); - - // Set optional size - if let Some(size) = size { - avatar.set_scale(size)?; - } - - avatar.export_png_data() - }) - .await?; - - // HTTP response - avatar - .map(FileFromBytes::from) - .map_err(Error::internal_server_error) -} - pub mod content { use jwt::JwtClaims; use rocket::serde::Serialize; @@ -92,68 +32,3 @@ pub mod content { pub user: JwtClaims, } } - -#[get("/")] -async fn redirect_to_setup(_setup: NeedSetup) -> Redirect { - Redirect::to(uri!(super::setup::setup)) -} - -#[get("/", rank = 2)] -async fn homepage(admin: JwtAdmin) -> Page { - Page::Homepage(content::Homepage { user: admin.0 }) -} - -#[get("/", rank = 3)] -async fn homepage_user(user: JwtUser) -> Page { - Page::Homepage(content::Homepage { user: user.0 }) -} - -#[get("/", rank = 4)] -async fn homepage_redirect() -> Redirect { - Redirect::to(uri!(super::oauth::authorize::authorize_ezidam)) -} - -#[post("/logout")] -async fn logout( - mut db: Connection, - refresh_token: RefreshToken, - cookie_jar: &CookieJar<'_>, -) -> Result { - let mut transaction = db.begin().await?; - - let refresh_token = refresh_tokens::RefreshToken::get_one(&mut transaction, &refresh_token.0) - .await? - .ok_or_else(|| Error::not_found("Unknown refresh token"))?; - - // Delete cookies - cookie_jar.remove(Cookie::named(JWT_COOKIE_NAME)); - cookie_jar.remove(Cookie::named(REFRESH_TOKEN_COOKIE_NAME)); - - // If refresh token has already been used - if refresh_token.has_been_used() { - // Revoke all refresh tokens for user - refresh_tokens::RefreshToken::revoke_all_for_user(&mut transaction, refresh_token.user()) - .await?; - - transaction.commit().await?; - - return Err(Error::forbidden("This refresh token has already been used")); - } - - // If token has been revoked - if refresh_token.is_revoked() { - return Err(Error::forbidden("This refresh token has been revoked")); - } - - // If token has expired - if refresh_token.has_expired() { - return Err(Error::forbidden("This refresh token has expired")); - } - - // Revoke token - refresh_token.revoke(&mut transaction).await?; - - transaction.commit().await?; - - Ok(Redirect::to(uri!(homepage))) -} diff --git a/crates/ezidam/src/routes/root/avatar.rs b/crates/ezidam/src/routes/root/avatar.rs new file mode 100644 index 0000000..a4f0efa --- /dev/null +++ b/crates/ezidam/src/routes/root/avatar.rs @@ -0,0 +1,34 @@ +use crate::routes::prelude::*; +use rocket::get; +use users::User; + +#[get("/avatar/?")] +pub async fn user_avatar( + mut db: Connection, + _user: JwtUser, + user_id: RocketUserID, + size: Option, +) -> Result { + // Verify existence of user + User::get_by_login(&mut *db, user_id.0.as_ref()) + .await? + .ok_or_else(|| Error::not_found(user_id.0.to_string()))?; + + // Generate avatar + let avatar = task::spawn_blocking(move || { + let mut avatar = identicon_rs::Identicon::new(user_id.0); + + // Set optional size + if let Some(size) = size { + avatar.set_scale(size)?; + } + + avatar.export_png_data() + }) + .await?; + + // HTTP response + avatar + .map(FileFromBytes::from) + .map_err(Error::internal_server_error) +} diff --git a/crates/ezidam/src/routes/root/home.rs b/crates/ezidam/src/routes/root/home.rs new file mode 100644 index 0000000..6f29f5e --- /dev/null +++ b/crates/ezidam/src/routes/root/home.rs @@ -0,0 +1,23 @@ +use super::content; +use crate::routes::prelude::*; +use rocket::get; + +#[get("/")] +pub async fn homepage(admin: JwtAdmin) -> Page { + Page::Homepage(content::Homepage { user: admin.0 }) +} + +#[get("/", rank = 2)] +pub async fn homepage_user(user: JwtUser) -> Page { + Page::Homepage(content::Homepage { user: user.0 }) +} + +#[get("/", rank = 3)] +pub async fn redirect_to_setup(_setup: NeedSetup) -> Redirect { + Redirect::to(uri!(crate::routes::setup::setup)) +} + +#[get("/", rank = 4)] +pub async fn homepage_redirect() -> Redirect { + Redirect::to(uri!(crate::routes::oauth::authorize::authorize_ezidam)) +} diff --git a/crates/ezidam/src/routes/root/logo.rs b/crates/ezidam/src/routes/root/logo.rs new file mode 100644 index 0000000..d7f8b30 --- /dev/null +++ b/crates/ezidam/src/routes/root/logo.rs @@ -0,0 +1,36 @@ +use crate::routes::prelude::*; +use rocket::get; +use settings::Settings; + +#[get("/logo")] +pub async fn get_logo(mut db: Connection) -> Result { + // Get settings + let settings = Settings::get(&mut *db).await?; + + // HTTP response + Ok(FileFromBytes::from(settings.business_logo())) +} + +#[cfg(test)] +mod test { + use crate::tests::*; + + #[test] + fn logo() { + // Setup http server + let client = setup_rocket_testing(); + + // Make request + let response = client.get(uri!(super::logo)).dispatch(); + assert_eq!(response.status(), Status::Ok); + + // Assert size of logo + let logo_length = response.into_bytes().expect("bytes containing logo").len(); + use settings::DEFAULT_BUSINESS_LOGO; + assert_eq!( + logo_length, + DEFAULT_BUSINESS_LOGO.len(), + "Invalid logo size in bytes, value was `{logo_length}`", + ); + } +} diff --git a/crates/ezidam/src/routes/root/logout.rs b/crates/ezidam/src/routes/root/logout.rs new file mode 100644 index 0000000..3c004dc --- /dev/null +++ b/crates/ezidam/src/routes/root/logout.rs @@ -0,0 +1,49 @@ +use crate::routes::prelude::*; +use crate::tokens::{JWT_COOKIE_NAME, REFRESH_TOKEN_COOKIE_NAME}; +use rocket::http::{Cookie, CookieJar}; +use rocket::post; + +#[post("/logout")] +pub async fn request_logout( + mut db: Connection, + refresh_token: RefreshToken, + cookie_jar: &CookieJar<'_>, +) -> Result { + let mut transaction = db.begin().await?; + + let refresh_token = refresh_tokens::RefreshToken::get_one(&mut transaction, &refresh_token.0) + .await? + .ok_or_else(|| Error::not_found("Unknown refresh token"))?; + + // Delete cookies + cookie_jar.remove(Cookie::named(JWT_COOKIE_NAME)); + cookie_jar.remove(Cookie::named(REFRESH_TOKEN_COOKIE_NAME)); + + // If refresh token has already been used + if refresh_token.has_been_used() { + // Revoke all refresh tokens for user + refresh_tokens::RefreshToken::revoke_all_for_user(&mut transaction, refresh_token.user()) + .await?; + + transaction.commit().await?; + + return Err(Error::forbidden("This refresh token has already been used")); + } + + // If token has been revoked + if refresh_token.is_revoked() { + return Err(Error::forbidden("This refresh token has been revoked")); + } + + // If token has expired + if refresh_token.has_expired() { + return Err(Error::forbidden("This refresh token has expired")); + } + + // Revoke token + refresh_token.revoke(&mut transaction).await?; + + transaction.commit().await?; + + Ok(Redirect::to(uri!(super::homepage))) +} diff --git a/crates/ezidam/src/routes/setup.rs b/crates/ezidam/src/routes/setup.rs index 90094fa..8f473a5 100644 --- a/crates/ezidam/src/routes/setup.rs +++ b/crates/ezidam/src/routes/setup.rs @@ -13,7 +13,7 @@ pub fn routes() -> Vec { #[get("/setup")] async fn setup_completed(_setup: CompletedSetup) -> Redirect { - Redirect::to(uri!(super::root::homepage)) + Redirect::to(uri!(super::root::home::homepage)) } #[get("/setup", rank = 2)] @@ -100,7 +100,9 @@ async fn create_first_account( transaction.commit().await?; // Redirect to home - Ok(Either::Left(Redirect::to(uri!(super::root::homepage)))) + Ok(Either::Left(Redirect::to(uri!( + super::root::home::homepage + )))) } #[cfg(test)]