server: Implement handling of households

This commit is contained in:
traxys 2023-05-29 00:15:55 +02:00
parent 66249a5e82
commit 9747260aab
6 changed files with 222 additions and 6 deletions

4
Cargo.lock generated
View file

@ -62,6 +62,7 @@ name = "api"
version = "0.1.0"
dependencies = [
"serde",
"uuid",
]
[[package]]
@ -2254,6 +2255,7 @@ dependencies = [
"jwt-simple",
"migration",
"sea-orm",
"sea-query",
"serde",
"sha2",
"thiserror",
@ -2261,6 +2263,7 @@ dependencies = [
"tower-http",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
@ -3358,6 +3361,7 @@ version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
dependencies = [
"getrandom",
"serde",
]

View file

@ -22,6 +22,8 @@ migration = { path = "./migration" }
thiserror = "1.0.40"
tower-http = { version = "0.4.0", features = ["cors", "fs"] }
sha2 = "0.10"
uuid = { version = "1.3", features = ["v4"] }
sea-query = "0.28"
[dependencies.sea-orm]
version = "0.11"

View file

@ -7,3 +7,4 @@ edition = "2021"
[dependencies]
serde = { version = "1.0.163", features = ["derive"] }
uuid = "1.3.3"

View file

@ -1,4 +1,7 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
pub struct LoginRequest {
@ -10,3 +13,32 @@ pub struct LoginRequest {
pub struct LoginResponse {
pub token: String,
}
#[derive(Serialize, Deserialize)]
pub struct EmptyResponse {}
#[derive(Serialize, Deserialize)]
pub struct Household {
pub name: String,
pub members: Vec<Uuid>,
}
#[derive(Serialize, Deserialize)]
pub struct Households {
pub households: HashMap<Uuid, Household>,
}
#[derive(Serialize, Deserialize)]
pub struct CreateHouseholdRequest {
pub name: String,
}
#[derive(Serialize, Deserialize)]
pub struct CreateHouseholdResponse {
pub id: Uuid,
}
#[derive(Serialize, Deserialize)]
pub struct AddToHouseholdRequest {
pub user: Uuid,
}

149
src/routes/household.rs Normal file
View 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 {}))
}

View file

@ -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])),
)
}