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> {
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),
},
}
}

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)]
pub struct SessionId(String);

View File

@ -101,11 +101,13 @@ where
Fut: Future<Output = (StatusCode, Json<Option<A>>)>,
{
match check_session(&core, headers).await {
ResultExt::Ok(Some(user)) => if user.admin {
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<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 {
(
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<Optio
#[derive(Deserialize, Serialize)]
#[typeshare]
pub struct CreateUserRequest {
username: String,
pub username: String,
}
pub async fn create_user(core: Core, headers: HeaderMap, req: CreateUserRequest) -> (StatusCode, Json<Option<()>>) {
auth_required(core.clone(), headers, |_admin| async {
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
})
.await
}
/*

View File

@ -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<UserId>, 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<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]
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<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!();
}