From 5bb9f00a0d24aca49661fbc8da4b22f647205d7e Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 2 Jan 2025 13:41:05 -0500 Subject: [PATCH] Extract the user management handlers --- visions/server/src/filters/mod.rs | 138 ------------- visions/server/src/filters/user_management.rs | 182 ------------------ .../server/src/handlers/user_management.rs | 166 ++++++++++++++++ visions/server/src/main.rs | 5 +- visions/server/src/types.rs | 19 +- 5 files changed, 186 insertions(+), 324 deletions(-) delete mode 100644 visions/server/src/filters/mod.rs delete mode 100644 visions/server/src/filters/user_management.rs create mode 100644 visions/server/src/handlers/user_management.rs diff --git a/visions/server/src/filters/mod.rs b/visions/server/src/filters/mod.rs deleted file mode 100644 index a433057..0000000 --- a/visions/server/src/filters/mod.rs +++ /dev/null @@ -1,138 +0,0 @@ -mod user_management; -// pub use user_management::routes_user_management; - -use crate::{ - asset_db::AssetId, - core::Core, -}; - - -// Per-endpoint Authentication: -// -// If an endpoint requires authentication: -// - check the Authorization header for a token -// - if the token is absent or unknown, return a 403 -// - if the admin user is absent, return a 403, with a body that indicates the admin user is absent -// -// The login function does not require authentication, but it should return a session ID - -/* -fn cors(methods: Vec, headers: Vec) -> Builder -where - M: Into, - H: Into, -{ - warp::cors() - .allow_credentials(true) - .allow_methods(methods) - .allow_headers(headers) -} -*/ - -/* -pub fn route_healthcheck() -> impl Filter + Clone -{ - warp::path!("api" / "v1" / "healthcheck") - .and(warp::get()) - .map(|| warp::reply::reply()) -} - -pub fn route_server_status( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "status") - .and(warp::get()) - .then(move || handle_server_status(core.clone())) -} - -pub fn route_set_bg_image( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "tabletop" / "bg_image") - .and(warp::put()) - .and(warp::body::json()) - .then({ - let core = core.clone(); - move |body| handle_set_background_image(core.clone(), body) - }) - .with(cors::(vec![Method::PUT], vec![])) -} - -pub fn route_image( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "image" / String) - .and(warp::get()) - .then({ - let core = core.clone(); - move |file_name| handle_file(core.clone(), AssetId::from(file_name)) - }) -} - -pub fn route_available_images( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "image").and(warp::get()).then({ - let core = core.clone(); - move || handle_available_images(core.clone()) - }) -} - -pub fn route_register_client( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "client") - .and(warp::post()) - .then({ - let core = core.clone(); - move || handle_register_client(core.clone(), RegisterRequest {}) - }) -} - -pub fn route_unregister_client( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "client" / String) - .and(warp::delete()) - .then({ - let core = core.clone(); - move |client_id| handle_unregister_client(core.clone(), client_id) - }) -} - -pub fn route_websocket( - core: Core, -) -> impl Filter + Clone { - warp::path("ws") - .and(warp::ws()) - .and(warp::path::param()) - .then({ - let core = core.clone(); - move |ws, client_id| handle_connect_websocket(core.clone(), ws, client_id) - }) -} - -pub fn route_get_charsheet( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "charsheet" / String) - .and(warp::get()) - .then({ - let core = core.clone(); - move |charid| handle_get_charsheet(core.clone(), charid) - }) -} - -pub fn route_authenticate( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "auth") - .and(warp::put()) - .and(warp::body::json()) - .then({ - let core = core.clone(); - move |body| handle_check_password(core.clone(), body) - }) - .with(cors::(vec![Method::PUT], vec![])) -} -*/ diff --git a/visions/server/src/filters/user_management.rs b/visions/server/src/filters/user_management.rs deleted file mode 100644 index 1a80bb9..0000000 --- a/visions/server/src/filters/user_management.rs +++ /dev/null @@ -1,182 +0,0 @@ -use std::{convert::Infallible, future::Future}; - -use crate::{ - core::Core, -}; - -/* -async fn handle_rejection(err: warp::Rejection) -> Result { - println!("handle_rejection: {:?}", err); - if let Some(Unauthorized) = err.find() { - Ok(warp::reply::with_status( - "".to_owned(), - StatusCode::UNAUTHORIZED, - )) - } else { - Ok(warp::reply::with_status( - "".to_owned(), - StatusCode::INTERNAL_SERVER_ERROR, - )) - } -} -*/ - -/* -#[derive(Debug)] -struct Unauthorized; - -impl reject::Reject for Unauthorized {} - -use super::cors; - -fn route_get_users( - core: Core, -) -> impl Filter>,), Error = warp::Rejection> + Clone { - warp::path!("api" / "v1" / "users") - .and(warp::get()) - .and(warp::header::optional::("authorization")) - .and(warp::any().map(move || core.clone())) - .and_then(|auth_token, core: Core| async move { - match auth_token { - Some(token) => Ok(handle_get_users(core.clone()).await), - None => Err(warp::reject::custom(Unauthorized)), - } - }) -} - -fn route_set_admin_password( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "admin_password") - .and(warp::put()) - .and(warp::body::json()) - .then({ move |body| handle_set_admin_password(core.clone(), body) }) - .with(cors(vec![Method::PUT], vec![CONTENT_TYPE])) -} - -pub fn routes_user_management( - core: Core, -) -> impl Filter + Clone { - route_get_users(core.clone()).or(route_set_admin_password(core.clone())) -} -*/ - -/* -pub fn route_check_password( - core: Core, -) -> impl Filter + Clone { - warp::path!("api" / "v1" / "auth") - .and(warp::put()) - .and(warp::body::json()) - .then({ move |body| handle_check_password(core.clone(), body) }) - .with(cors::(vec![Method::PUT], vec![])) -} -*/ - -/* -#[cfg(test)] -mod test { - use std::{collections::HashMap, path::PathBuf}; - - use result_extended::ResultExt; - use warp::http::StatusCode; - - use crate::{ - asset_db::mocks::MemoryAssets, - database::{Database, DbConn, UserId}, - }; - - use super::*; - - async fn setup() -> Core { - let asset_store = MemoryAssets::new(vec![]); - let memory_file: Option = None; - let db = DbConn::new(memory_file); - db.save_user(None, "admin", "", true, true).await.unwrap(); - Core::new(asset_store, db) - } - - #[tokio::test] - async fn handle_check_password_should_return_a_valid_token() { - let core = setup().await; - match core.list_users().await { - ResultExt::Ok(users) => println!("{:?}", users), - ResultExt::Err(err) => panic!("{}", err), - ResultExt::Fatal(err) => panic!("{}", err), - } - match core.user_by_username("admin").await { - ResultExt::Ok(Some(user)) => { - let _ = core - .set_password(UserId::from(user.id), "aoeu".to_owned()) - .await; - } - ResultExt::Ok(None) => panic!("expected user wasn't found"), - ResultExt::Err(err) => panic!("{}", err), - ResultExt::Fatal(err) => panic!("{}", err), - } - let filter = route_check_password(core); - let params: HashMap = vec![ - ("username".to_owned(), "admin".to_owned()), - ("password".to_owned(), "aoeu".to_owned()), - ] - .into_iter() - .collect(); - let resp = warp::test::request() - .method("PUT") - .path("/api/v1/auth") - .json(¶ms) - .reply(&filter) - .await; - - println!("response: {}", resp.status()); - assert!(resp.status().is_success()); - println!( - "resp.body(): {}", - String::from_utf8(resp.body().to_vec()).unwrap() - ); - serde_json::from_slice::(resp.body()).unwrap(); - } - - /* - #[tokio::test] - async fn handle_check_auth_token() { - let core = setup().await; - let filter = route_get_users(core); - let response = warp::test::request() - .method("GET") - .path("/api/v1/users") - .header("Authorization", "abcdefg") - .reply(&filter) - .await; - - println!("response: {}", response.status()); - assert!(false); - } - */ - - #[tokio::test] - async fn it_rejects_unauthorized_requests() { - let core = setup().await; - let filter = route_get_users(core) - .recover(handle_rejection); - let response = warp::test::request() - .method("GET") - .path("/api/v1/users") - .reply(&filter) - .await; - - println!("response: {:?}", response); - assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - } - - #[tokio::test] - async fn it_accepts_authorized_requests() { - unimplemented!(); - } - - #[tokio::test] - async fn it_returns_special_response_for_no_admin() { - unimplemented!(); - } -} -*/ diff --git a/visions/server/src/handlers/user_management.rs b/visions/server/src/handlers/user_management.rs new file mode 100644 index 0000000..1634b01 --- /dev/null +++ b/visions/server/src/handlers/user_management.rs @@ -0,0 +1,166 @@ +use serde::{Deserialize, Serialize}; + +use crate::database::UserId; + + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[typeshare] +pub struct AuthRequest { + pub username: String, + pub password: String, +} + +#[derive(Deserialize, Serialize)] +#[typeshare] +pub struct CreateUserRequest { + pub username: String, +} + +#[derive(Deserialize, Serialize)] +#[typeshare] +pub struct SetPasswordRequest { + pub password_1: String, + pub password_2: String, +} + +async fn check_session( + core: &Core, + headers: HeaderMap, +) -> ResultExt, AppError, FatalError> { + match headers.get("Authorization") { + Some(token) => { + match token + .to_str() + .unwrap() + .split(" ") + .collect::>() + .as_slice() + { + [_schema, token] => core.session(&SessionId::from(token.to_owned())).await, + _ => error(AppError::BadRequest), + } + } + None => ok(None), + } +} + +pub async fn auth_required( + core: Core, + headers: HeaderMap, + f: F, +) -> (StatusCode, Json>) +where + F: FnOnce(User) -> Fut, + Fut: Future>)>, +{ + match check_session(&core, headers).await { + ResultExt::Ok(Some(user)) => f(user).await, + ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), + ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Fatal(err) => panic!("{}", err), + } +} + +pub async fn admin_required( + core: Core, + headers: HeaderMap, + f: F, +) -> (StatusCode, Json>) +where + F: FnOnce(User) -> Fut, + Fut: Future>)>, +{ + match check_session(&core, headers).await { + ResultExt::Ok(Some(user)) => { + if user.admin { + f(user).await + } else { + (StatusCode::FORBIDDEN, Json(None)) + } + } + ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), + ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Fatal(err) => panic!("{}", err), + } +} + +pub async fn check_password( + core: Core, + req: Json, +) -> (StatusCode, Json>) { + let Json(AuthRequest { username, password }) = req; + match core.auth(&username, &password).await { + ResultExt::Ok(session_id) => (StatusCode::OK, Json(Some(session_id))), + ResultExt::Err(_err) => (StatusCode::UNAUTHORIZED, Json(None)), + ResultExt::Fatal(err) => panic!("Fatal: {}", err), + } +} + +pub async fn get_user( + core: Core, + headers: HeaderMap, + user_id: Option, +) -> (StatusCode, Json>) { + auth_required(core.clone(), headers, |user| async move { + match user_id { + Some(user_id) => match core.user(user_id).await { + ResultExt::Ok(Some(user)) => ( + StatusCode::OK, + Json(Some(UserProfile { + userid: UserId::from(user.id), + username: user.name, + is_admin: user.admin, + })), + ), + ResultExt::Ok(None) => (StatusCode::NOT_FOUND, Json(None)), + ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Fatal(err) => panic!("{}", err), + }, + None => ( + StatusCode::OK, + Json(Some(UserProfile { + userid: UserId::from(user.id), + username: user.name, + is_admin: user.admin, + })), + ), + } + }) + .await +} + +pub async fn create_user( + core: Core, + headers: HeaderMap, + req: CreateUserRequest, +) -> (StatusCode, Json>) { + admin_required(core.clone(), headers, |_admin| async { + match core.create_user(&req.username).await { + ResultExt::Ok(_) => (StatusCode::OK, Json(None)), + ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Fatal(fatal) => panic!("{}", fatal), + } + }) + .await +} + +pub async fn set_password( + core: Core, + headers: HeaderMap, + req: SetPasswordRequest, +) -> (StatusCode, Json>) { + auth_required(core.clone(), headers, |user| async { + if req.password_1 == req.password_2 { + match core.set_password(user.id, req.password_1).await { + ResultExt::Ok(_) => (StatusCode::OK, Json(None)), + ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Fatal(fatal) => panic!("{}", fatal), + } + } else { + (StatusCode::BAD_REQUEST, Json(None)) + } + }) + .await +} + + diff --git a/visions/server/src/main.rs b/visions/server/src/main.rs index 84524e6..71032a2 100644 --- a/visions/server/src/main.rs +++ b/visions/server/src/main.rs @@ -7,9 +7,8 @@ use database::DbConn; mod asset_db; mod core; mod database; -mod filters; -pub mod handlers; -pub mod routes; +mod handlers; +mod routes; mod types; #[tokio::main] diff --git a/visions/server/src/types.rs b/visions/server/src/types.rs index 07c7b0a..a9d5f84 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use typeshare::typeshare; -use crate::{asset_db::AssetId, database::{UserId, UserRow}}; +use crate::{asset_db::AssetId, database::{GameId, UserId, UserRow}}; #[derive(Debug, Error)] pub enum FatalError { @@ -129,4 +129,21 @@ pub enum Message { UpdateTabletop(Tabletop), } +#[derive(Deserialize, Serialize)] +#[typeshare] +pub struct UserProfile { + pub id: UserId, + pub username: String, + pub games: Vec, + pub is_admin: bool, +} + +#[derive(Deserialize, Serialize)] +#[typeshare] +pub struct GameOverview { + pub id: GameId, + pub game_type: String, + pub game_name: String, +} +