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"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2254,6 +2255,7 @@ dependencies = [
|
||||||
"jwt-simple",
|
"jwt-simple",
|
||||||
"migration",
|
"migration",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
|
"sea-query",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
@ -2261,6 +2263,7 @@ dependencies = [
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3358,6 +3361,7 @@ version = "1.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ migration = { path = "./migration" }
|
||||||
thiserror = "1.0.40"
|
thiserror = "1.0.40"
|
||||||
tower-http = { version = "0.4.0", features = ["cors", "fs"] }
|
tower-http = { version = "0.4.0", features = ["cors", "fs"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
uuid = { version = "1.3", features = ["v4"] }
|
||||||
|
sea-query = "0.28"
|
||||||
|
|
||||||
[dependencies.sea-orm]
|
[dependencies.sea-orm]
|
||||||
version = "0.11"
|
version = "0.11"
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.163", features = ["derive"] }
|
serde = { version = "1.0.163", features = ["derive"] }
|
||||||
|
uuid = "1.3.3"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct LoginRequest {
|
pub struct LoginRequest {
|
||||||
|
|
@ -10,3 +13,32 @@ pub struct LoginRequest {
|
||||||
pub struct LoginResponse {
|
pub struct LoginResponse {
|
||||||
pub token: String,
|
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},
|
headers::{authorization::Bearer, Authorization},
|
||||||
http::{header::CONTENT_TYPE, request::Parts, HeaderValue, Method, StatusCode},
|
http::{header::CONTENT_TYPE, request::Parts, HeaderValue, Method, StatusCode},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::post,
|
routing::{get, post, put},
|
||||||
Json, Router, TypedHeader,
|
Json, Router, TypedHeader,
|
||||||
};
|
};
|
||||||
use jwt_simple::prelude::*;
|
use jwt_simple::prelude::*;
|
||||||
|
|
@ -17,6 +17,8 @@ use tower_http::cors::{self, AllowOrigin, CorsLayer};
|
||||||
|
|
||||||
use crate::entity::{prelude::*, user};
|
use crate::entity::{prelude::*, user};
|
||||||
|
|
||||||
|
mod household;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
enum RouteError {
|
enum RouteError {
|
||||||
#[error("This account does not exist")]
|
#[error("This account does not exist")]
|
||||||
|
|
@ -29,6 +31,10 @@ enum RouteError {
|
||||||
UserJwt(jwt_simple::Error),
|
UserJwt(jwt_simple::Error),
|
||||||
#[error("Request is missing the bearer token")]
|
#[error("Request is missing the bearer token")]
|
||||||
MissingAuthorization,
|
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 {
|
impl IntoResponse for RouteError {
|
||||||
|
|
@ -44,6 +50,12 @@ impl IntoResponse for RouteError {
|
||||||
tracing::debug!("Invalid user JWT: {e:?}");
|
tracing::debug!("Invalid user JWT: {e:?}");
|
||||||
(StatusCode::BAD_REQUEST, "Invalid authorization header").into_response()
|
(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 => {
|
e => {
|
||||||
tracing::error!("Internal error: {e:?}");
|
tracing::error!("Internal error: {e:?}");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
|
|
@ -58,7 +70,7 @@ type AppState = Arc<crate::AppState>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct AuthenticatedUser {
|
struct AuthenticatedUser {
|
||||||
pub id: Uuid,
|
pub model: user::Model,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -85,9 +97,12 @@ where
|
||||||
.verify_token::<NoCustomClaims>(bearer.token(), None)
|
.verify_token::<NoCustomClaims>(bearer.token(), None)
|
||||||
.map_err(RouteError::UserJwt)?;
|
.map_err(RouteError::UserJwt)?;
|
||||||
|
|
||||||
Ok(AuthenticatedUser {
|
let model = User::find_by_id(claims.subject.unwrap().parse::<Uuid>().unwrap())
|
||||||
id: claims.subject.unwrap().parse().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);
|
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