server: Implement handling of households
This commit is contained in:
parent
66249a5e82
commit
9747260aab
6 changed files with 222 additions and 6 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
uuid = "1.3.3"
|
||||
|
|
|
|||
|
|
@ -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
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