Extract the user management handlers

This commit is contained in:
Savanni D'Gerinel 2025-01-02 13:41:05 -05:00
parent f6eb942371
commit 5bb9f00a0d
5 changed files with 186 additions and 324 deletions

View File

@ -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<H, M>(methods: Vec<M>, headers: Vec<H>) -> Builder
where
M: Into<Method>,
H: Into<HeaderName>,
{
warp::cors()
.allow_credentials(true)
.allow_methods(methods)
.allow_headers(headers)
}
*/
/*
pub fn route_healthcheck() -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone
{
warp::path!("api" / "v1" / "healthcheck")
.and(warp::get())
.map(|| warp::reply::reply())
}
pub fn route_server_status(
core: Core,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + 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<Extract = (impl Reply,), Error = warp::Rejection> + 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::<HeaderName, Method>(vec![Method::PUT], vec![]))
}
pub fn route_image(
core: Core,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + 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<Extract = (impl Reply,), Error = warp::Rejection> + 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<Extract = (impl Reply,), Error = warp::Rejection> + 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<Extract = (impl Reply,), Error = warp::Rejection> + 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<Extract = (impl Reply,), Error = warp::Rejection> + 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<Extract = (impl Reply,), Error = warp::Rejection> + 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<Extract = (impl Reply,), Error = warp::Rejection> + 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::<HeaderName, Method>(vec![Method::PUT], vec![]))
}
*/

View File

@ -1,182 +0,0 @@
use std::{convert::Infallible, future::Future};
use crate::{
core::Core,
};
/*
async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible> {
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<Extract = (Response<Vec<u8>>,), Error = warp::Rejection> + Clone {
warp::path!("api" / "v1" / "users")
.and(warp::get())
.and(warp::header::optional::<String>("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<Extract = (impl Reply,), Error = warp::Rejection> + 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<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
route_get_users(core.clone()).or(route_set_admin_password(core.clone()))
}
*/
/*
pub fn route_check_password(
core: Core,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
warp::path!("api" / "v1" / "auth")
.and(warp::put())
.and(warp::body::json())
.then({ move |body| handle_check_password(core.clone(), body) })
.with(cors::<HeaderName, Method>(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<PathBuf> = 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<String, String> = 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(&params)
.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::<String>(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!();
}
}
*/

View File

@ -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<Option<User>, AppError, FatalError> {
match headers.get("Authorization") {
Some(token) => {
match token
.to_str()
.unwrap()
.split(" ")
.collect::<Vec<&str>>()
.as_slice()
{
[_schema, token] => core.session(&SessionId::from(token.to_owned())).await,
_ => error(AppError::BadRequest),
}
}
None => ok(None),
}
}
pub async fn auth_required<F, A, Fut>(
core: Core,
headers: HeaderMap,
f: F,
) -> (StatusCode, Json<Option<A>>)
where
F: FnOnce(User) -> Fut,
Fut: Future<Output = (StatusCode, Json<Option<A>>)>,
{
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<F, A, Fut>(
core: Core,
headers: HeaderMap,
f: F,
) -> (StatusCode, Json<Option<A>>)
where
F: FnOnce(User) -> Fut,
Fut: Future<Output = (StatusCode, Json<Option<A>>)>,
{
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<AuthRequest>,
) -> (StatusCode, Json<Option<SessionId>>) {
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<UserId>,
) -> (StatusCode, Json<Option<UserProfile>>) {
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<Option<()>>) {
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<Option<()>>) {
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
}

View File

@ -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]

View File

@ -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<GameOverview>,
pub is_admin: bool,
}
#[derive(Deserialize, Serialize)]
#[typeshare]
pub struct GameOverview {
pub id: GameId,
pub game_type: String,
pub game_name: String,
}