diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index ad90579..2535458 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -126,11 +126,19 @@ impl Core { } } + pub async fn user(&self, user_id: UserId) -> ResultExt, AppError, FatalError> { + let users = return_error!(self.list_users().await); + ok(users.into_iter().find(|user| user.id == user_id)) + } + pub async fn create_user(&self, username: &str) -> ResultExt<(), AppError, FatalError> { let state = self.0.read().await; match return_error!(self.user_by_username(username).await) { Some(_) => error(AppError::UsernameUnavailable), - None => unimplemented!(), + None => match state.db.save_user(None, username, "", false, true).await { + Ok(_) => ok(()), + Err(err) => fatal(err), + }, } } diff --git a/visions/server/src/database/types.rs b/visions/server/src/database/types.rs index 9bb9283..8e2daef 100644 --- a/visions/server/src/database/types.rs +++ b/visions/server/src/database/types.rs @@ -40,6 +40,12 @@ impl FromSql for UserId { } } +impl fmt::Display for UserId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str(&self.0) + } +} + #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct SessionId(String); diff --git a/visions/server/src/handlers/mod.rs b/visions/server/src/handlers/mod.rs index 37d35bf..0900855 100644 --- a/visions/server/src/handlers/mod.rs +++ b/visions/server/src/handlers/mod.rs @@ -95,17 +95,19 @@ pub async fn admin_required( core: Core, headers: HeaderMap, f: F, - ) -> (StatusCode, Json>) +) -> (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(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), @@ -120,16 +122,35 @@ pub struct UserProfile { pub is_admin: bool, } -pub async fn get_user(core: Core, headers: HeaderMap) -> (StatusCode, Json>) { +pub async fn get_user( + core: Core, + headers: HeaderMap, + user_id: Option, +) -> (StatusCode, Json>) { auth_required(core.clone(), headers, |user| async move { - ( - StatusCode::OK, - Json(Some(UserProfile { - userid: UserId::from(user.id), - username: user.name, - is_admin: user.admin, - })), - ) + 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 } @@ -137,17 +158,22 @@ pub async fn get_user(core: Core, headers: HeaderMap) -> (StatusCode, Json (StatusCode, Json>) { - auth_required(core.clone(), headers, |_admin| async { +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 + }) + .await } /* diff --git a/visions/server/src/routes.rs b/visions/server/src/routes.rs index 3e71dda..86f6474 100644 --- a/visions/server/src/routes.rs +++ b/visions/server/src/routes.rs @@ -1,6 +1,7 @@ use std::fmt; use axum::{ + extract::Path, http::{HeaderMap, StatusCode}, routing::{get, post}, Json, Router, @@ -8,6 +9,7 @@ use axum::{ use crate::{ core::Core, + database::UserId, handlers::{ check_password, create_user, get_user, healthcheck, AuthRequest, CreateUserRequest, }, @@ -34,7 +36,7 @@ pub fn routes(core: Core) -> Router { "/api/v1/user", get({ let core = core.clone(); - move |headers: HeaderMap| get_user(core, headers) + move |headers: HeaderMap| get_user(core, headers, None) }) .put({ let core = core.clone(); @@ -44,6 +46,16 @@ pub fn routes(core: Core) -> Router { } }), ) + .route( + "/api/v1/user/:user_id", + get({ + let core = core.clone(); + move |user_id: Path, headers: HeaderMap| { + let Path(user_id) = user_id; + get_user(core, headers, Some(user_id)) + } + }), + ) } #[cfg(test)] @@ -84,6 +96,32 @@ mod test { (core, server) } + async fn setup_with_user() -> (Core, TestServer) { + let (core, server) = setup_admin_enabled().await; + let response = server + .post("/api/v1/auth") + .json(&AuthRequest { + username: "admin".to_owned(), + password: "aoeu".to_owned(), + }) + .await; + response.assert_status_ok(); + let session_id: Option = response.json(); + let session_id = session_id.unwrap(); + + let response = server + .put("/api/v1/user") + .add_header("Authorization", format!("Bearer {}", session_id)) + .json(&CreateUserRequest { + username: "savanni".to_owned(), + }) + .await; + + response.assert_status_ok(); + + (core, server) + } + #[tokio::test] async fn it_returns_a_healthcheck() { let (core, server) = setup_without_admin(); @@ -170,6 +208,54 @@ mod test { #[tokio::test] async fn an_admin_can_create_a_user() { + // All of the contents of this test are basically required for any test on individual + // users, so I moved it all into the setup code. + let (_core, _server) = setup_with_user().await; + } + + #[tokio::test] + async fn a_user_can_get_any_user_profile() { + let (core, server) = setup_with_user().await; + + let savanni = match core.user_by_username("savanni").await { + ResultExt::Ok(Some(savanni)) => savanni, + ResultExt::Ok(None) => panic!("user was not initialized"), + ResultExt::Err(err) => panic!("{:?}", err), + ResultExt::Fatal(err) => panic!("{:?}", err), + }; + + let response = server + .post("/api/v1/auth") + .json(&AuthRequest { + username: "savanni".to_owned(), + password: "".to_owned(), + }) + .await; + let session_id: Option = response.json(); + let session_id = session_id.unwrap(); + + let response = server + .get(&format!("/api/v1/user/{}", savanni.id)) + .add_header("Authorization", format!("Bearer {}", session_id)) + .await; + response.assert_status_ok(); + let profile: Option = response.json(); + let profile = profile.unwrap(); + assert_eq!(profile.username, "savanni"); + + let response = server + .get("/api/v1/user/admin") + .add_header("Authorization", format!("Bearer {}", session_id)) + .await; + response.assert_status_ok(); + let profile: Option = response.json(); + let profile = profile.unwrap(); + assert_eq!(profile.username, "admin"); + } + + #[ignore] + #[tokio::test] + async fn a_user_can_get_change_their_password() { unimplemented!(); }