server: Implement handling of households
This commit is contained in:
parent
66249a5e82
commit
9747260aab
6 changed files with 222 additions and 6 deletions
149
src/routes/household.rs
Normal file
149
src/routes/household.rs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
extract::{FromRef, FromRequestParts, Path, State},
|
||||
http::request::Parts,
|
||||
Json,
|
||||
};
|
||||
use sea_orm::{prelude::*, ActiveValue};
|
||||
use sea_query::OnConflict;
|
||||
|
||||
use api::{
|
||||
AddToHouseholdRequest, CreateHouseholdRequest, CreateHouseholdResponse, EmptyResponse,
|
||||
Households,
|
||||
};
|
||||
|
||||
use super::{AppState, AuthenticatedUser, RouteError};
|
||||
use crate::entity::{household, household_members, prelude::*};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct AuthorizedHousehold(Uuid);
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for AuthorizedHousehold
|
||||
where
|
||||
S: Send + Sync,
|
||||
AppState: FromRef<S>,
|
||||
{
|
||||
type Rejection = RouteError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let State(app_state): State<AppState> = State::from_request_parts(parts, state)
|
||||
.await
|
||||
.expect("Could not get state");
|
||||
|
||||
let user = AuthenticatedUser::from_request_parts(parts, state).await?;
|
||||
|
||||
let Path(household): Path<Uuid> = Path::from_request_parts(parts, state).await?;
|
||||
|
||||
let matching_count = user
|
||||
.model
|
||||
.find_related(Household)
|
||||
.filter(household::Column::Id.eq(household))
|
||||
.count(&app_state.db)
|
||||
.await?;
|
||||
|
||||
match matching_count {
|
||||
0 => Err(RouteError::Unauthorized),
|
||||
_ => Ok(AuthorizedHousehold(household)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn list(
|
||||
user: AuthenticatedUser,
|
||||
state: State<AppState>,
|
||||
) -> super::JsonResult<Households> {
|
||||
let related_households = user.model.find_related(Household).all(&state.db).await?;
|
||||
|
||||
let mut households = HashMap::new();
|
||||
|
||||
for household in related_households {
|
||||
let members = household.find_related(User).all(&state.db).await?;
|
||||
households.insert(
|
||||
household.id,
|
||||
api::Household {
|
||||
name: household.name,
|
||||
members: members.into_iter().map(|m| m.id).collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Json(Households { households }))
|
||||
}
|
||||
|
||||
pub(super) async fn create(
|
||||
user: AuthenticatedUser,
|
||||
state: State<AppState>,
|
||||
Json(request): Json<CreateHouseholdRequest>,
|
||||
) -> super::JsonResult<CreateHouseholdResponse> {
|
||||
let household = household::ActiveModel {
|
||||
name: ActiveValue::Set(request.name),
|
||||
id: ActiveValue::Set(Uuid::new_v4()),
|
||||
};
|
||||
|
||||
let household = household.insert(&state.db).await?;
|
||||
|
||||
let member = household_members::ActiveModel {
|
||||
household: ActiveValue::Set(household.id),
|
||||
user: ActiveValue::Set(user.model.id),
|
||||
};
|
||||
|
||||
member.insert(&state.db).await?;
|
||||
|
||||
Ok(Json(CreateHouseholdResponse { id: household.id }))
|
||||
}
|
||||
|
||||
pub(super) async fn add_member(
|
||||
AuthorizedHousehold(household): AuthorizedHousehold,
|
||||
state: State<AppState>,
|
||||
Json(request): Json<AddToHouseholdRequest>,
|
||||
) -> super::JsonResult<EmptyResponse> {
|
||||
let member = household_members::ActiveModel {
|
||||
household: ActiveValue::Set(household),
|
||||
user: ActiveValue::Set(request.user),
|
||||
};
|
||||
|
||||
if let Err(e) = HouseholdMembers::insert(member)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
household_members::Column::Household,
|
||||
household_members::Column::User,
|
||||
])
|
||||
.do_nothing()
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&state.db)
|
||||
.await
|
||||
{
|
||||
if !matches!(e, DbErr::RecordNotInserted) {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(EmptyResponse {}))
|
||||
}
|
||||
|
||||
pub(super) async fn leave(
|
||||
AuthorizedHousehold(household): AuthorizedHousehold,
|
||||
user: AuthenticatedUser,
|
||||
state: State<AppState>,
|
||||
) -> super::JsonResult<EmptyResponse> {
|
||||
HouseholdMembers::delete_by_id((household, user.model.id))
|
||||
.exec(&state.db)
|
||||
.await?;
|
||||
|
||||
let Some(household) = Household::find_by_id(household)
|
||||
.one(&state.db)
|
||||
.await? else {
|
||||
return Ok(Json(EmptyResponse {}));
|
||||
};
|
||||
|
||||
let member_count = household.find_related(User).count(&state.db).await?;
|
||||
if member_count == 0 {
|
||||
household.delete(&state.db).await?;
|
||||
}
|
||||
|
||||
Ok(Json(EmptyResponse {}))
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ use axum::{
|
|||
headers::{authorization::Bearer, Authorization},
|
||||
http::{header::CONTENT_TYPE, request::Parts, HeaderValue, Method, StatusCode},
|
||||
response::IntoResponse,
|
||||
routing::post,
|
||||
routing::{get, post, put},
|
||||
Json, Router, TypedHeader,
|
||||
};
|
||||
use jwt_simple::prelude::*;
|
||||
|
|
@ -17,6 +17,8 @@ use tower_http::cors::{self, AllowOrigin, CorsLayer};
|
|||
|
||||
use crate::entity::{prelude::*, user};
|
||||
|
||||
mod household;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum RouteError {
|
||||
#[error("This account does not exist")]
|
||||
|
|
@ -29,6 +31,10 @@ enum RouteError {
|
|||
UserJwt(jwt_simple::Error),
|
||||
#[error("Request is missing the bearer token")]
|
||||
MissingAuthorization,
|
||||
#[error("User tried to edit an unauthorized ressource")]
|
||||
Unauthorized,
|
||||
#[error("Could not fetch required value from path")]
|
||||
PathRejection(#[from] axum::extract::rejection::PathRejection),
|
||||
}
|
||||
|
||||
impl IntoResponse for RouteError {
|
||||
|
|
@ -44,6 +50,12 @@ impl IntoResponse for RouteError {
|
|||
tracing::debug!("Invalid user JWT: {e:?}");
|
||||
(StatusCode::BAD_REQUEST, "Invalid authorization header").into_response()
|
||||
}
|
||||
RouteError::PathRejection(p) => p.into_response(),
|
||||
RouteError::Unauthorized => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Unauthorized to access this ressource",
|
||||
)
|
||||
.into_response(),
|
||||
e => {
|
||||
tracing::error!("Internal error: {e:?}");
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
|
|
@ -58,7 +70,7 @@ type AppState = Arc<crate::AppState>;
|
|||
|
||||
#[derive(Debug)]
|
||||
struct AuthenticatedUser {
|
||||
pub id: Uuid,
|
||||
pub model: user::Model,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
@ -85,9 +97,12 @@ where
|
|||
.verify_token::<NoCustomClaims>(bearer.token(), None)
|
||||
.map_err(RouteError::UserJwt)?;
|
||||
|
||||
Ok(AuthenticatedUser {
|
||||
id: claims.subject.unwrap().parse().unwrap(),
|
||||
})
|
||||
let model = User::find_by_id(claims.subject.unwrap().parse::<Uuid>().unwrap())
|
||||
.one(&app_state.db)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
Ok(AuthenticatedUser { model })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -131,5 +146,18 @@ pub(crate) fn router(api_allowed: Option<HeaderValue>) -> Router<AppState> {
|
|||
|
||||
let mk_service = |m: Vec<Method>| cors_base.clone().allow_methods(m);
|
||||
|
||||
Router::new().route("/login", post(login).layer(mk_service(vec![Method::POST])))
|
||||
Router::new()
|
||||
.route("/login", post(login).layer(mk_service(vec![Method::POST])))
|
||||
.route(
|
||||
"/household",
|
||||
get(household::list)
|
||||
.post(household::create)
|
||||
.layer(mk_service(vec![Method::GET, Method::POST])),
|
||||
)
|
||||
.route(
|
||||
"/household/:id",
|
||||
put(household::add_member)
|
||||
.delete(household::leave)
|
||||
.layer(mk_service(vec![Method::PUT, Method::DELETE])),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue