Add the ability to create users and to get profiles

This commit is contained in:
Savanni D'Gerinel 2024-12-31 23:47:40 -05:00
parent b2a7577c9d
commit d9f1efb8d3
4 changed files with 147 additions and 21 deletions

View File

@ -126,11 +126,19 @@ impl Core {
} }
} }
pub async fn user(&self, user_id: UserId) -> ResultExt<Option<User>, 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> { pub async fn create_user(&self, username: &str) -> ResultExt<(), AppError, FatalError> {
let state = self.0.read().await; let state = self.0.read().await;
match return_error!(self.user_by_username(username).await) { match return_error!(self.user_by_username(username).await) {
Some(_) => error(AppError::UsernameUnavailable), Some(_) => error(AppError::UsernameUnavailable),
None => unimplemented!(), None => match state.db.save_user(None, username, "", false, true).await {
Ok(_) => ok(()),
Err(err) => fatal(err),
},
} }
} }

View File

@ -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)] #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct SessionId(String); pub struct SessionId(String);

View File

@ -95,17 +95,19 @@ pub async fn admin_required<F, A, Fut>(
core: Core, core: Core,
headers: HeaderMap, headers: HeaderMap,
f: F, f: F,
) -> (StatusCode, Json<Option<A>>) ) -> (StatusCode, Json<Option<A>>)
where where
F: FnOnce(User) -> Fut, F: FnOnce(User) -> Fut,
Fut: Future<Output = (StatusCode, Json<Option<A>>)>, Fut: Future<Output = (StatusCode, Json<Option<A>>)>,
{ {
match check_session(&core, headers).await { match check_session(&core, headers).await {
ResultExt::Ok(Some(user)) => if user.admin { ResultExt::Ok(Some(user)) => {
f(user).await if user.admin {
} else { f(user).await
(StatusCode::FORBIDDEN, Json(None)) } else {
}, (StatusCode::FORBIDDEN, Json(None))
}
}
ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)),
ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)),
ResultExt::Fatal(err) => panic!("{}", err), ResultExt::Fatal(err) => panic!("{}", err),
@ -120,16 +122,35 @@ pub struct UserProfile {
pub is_admin: bool, pub is_admin: bool,
} }
pub async fn get_user(core: Core, headers: HeaderMap) -> (StatusCode, Json<Option<UserProfile>>) { 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 { auth_required(core.clone(), headers, |user| async move {
( match user_id {
StatusCode::OK, Some(user_id) => match core.user(user_id).await {
Json(Some(UserProfile { ResultExt::Ok(Some(user)) => (
userid: UserId::from(user.id), StatusCode::OK,
username: user.name, Json(Some(UserProfile {
is_admin: user.admin, 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 .await
} }
@ -137,17 +158,22 @@ pub async fn get_user(core: Core, headers: HeaderMap) -> (StatusCode, Json<Optio
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
#[typeshare] #[typeshare]
pub struct CreateUserRequest { pub struct CreateUserRequest {
username: String, pub username: String,
} }
pub async fn create_user(core: Core, headers: HeaderMap, req: CreateUserRequest) -> (StatusCode, Json<Option<()>>) { pub async fn create_user(
auth_required(core.clone(), headers, |_admin| async { core: Core,
headers: HeaderMap,
req: CreateUserRequest,
) -> (StatusCode, Json<Option<()>>) {
admin_required(core.clone(), headers, |_admin| async {
match core.create_user(&req.username).await { match core.create_user(&req.username).await {
ResultExt::Ok(_) => (StatusCode::OK, Json(None)), ResultExt::Ok(_) => (StatusCode::OK, Json(None)),
ResultExt::Err(err) => (StatusCode::BAD_REQUEST, Json(None)), ResultExt::Err(err) => (StatusCode::BAD_REQUEST, Json(None)),
ResultExt::Fatal(fatal) => panic!("{}", fatal), ResultExt::Fatal(fatal) => panic!("{}", fatal),
} }
}).await })
.await
} }
/* /*

View File

@ -1,6 +1,7 @@
use std::fmt; use std::fmt;
use axum::{ use axum::{
extract::Path,
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
routing::{get, post}, routing::{get, post},
Json, Router, Json, Router,
@ -8,6 +9,7 @@ use axum::{
use crate::{ use crate::{
core::Core, core::Core,
database::UserId,
handlers::{ handlers::{
check_password, create_user, get_user, healthcheck, AuthRequest, CreateUserRequest, check_password, create_user, get_user, healthcheck, AuthRequest, CreateUserRequest,
}, },
@ -34,7 +36,7 @@ pub fn routes(core: Core) -> Router {
"/api/v1/user", "/api/v1/user",
get({ get({
let core = core.clone(); let core = core.clone();
move |headers: HeaderMap| get_user(core, headers) move |headers: HeaderMap| get_user(core, headers, None)
}) })
.put({ .put({
let core = core.clone(); 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<UserId>, headers: HeaderMap| {
let Path(user_id) = user_id;
get_user(core, headers, Some(user_id))
}
}),
)
} }
#[cfg(test)] #[cfg(test)]
@ -84,6 +96,32 @@ mod test {
(core, server) (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<SessionId> = 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] #[tokio::test]
async fn it_returns_a_healthcheck() { async fn it_returns_a_healthcheck() {
let (core, server) = setup_without_admin(); let (core, server) = setup_without_admin();
@ -170,6 +208,54 @@ mod test {
#[tokio::test] #[tokio::test]
async fn an_admin_can_create_a_user() { 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<SessionId> = 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<UserProfile> = 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<UserProfile> = response.json();
let profile = profile.unwrap();
assert_eq!(profile.username, "admin");
}
#[ignore]
#[tokio::test]
async fn a_user_can_get_change_their_password() {
unimplemented!(); unimplemented!();
} }