From fa660a8cb68f79346e4dafb073e551f51a1504a2 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Fri, 27 Oct 2023 23:14:00 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=83=A0=E3=81=AE=E8=B3=AA=E5=95=8F=E3=82=92=E5=8F=96=E5=BE=97?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/src/repository/form_repository.rs | 3 +- server/entrypoint/src/main.rs | 5 +- .../infra/resource/src/database/components.rs | 3 +- server/infra/resource/src/database/form.rs | 46 +++++++++++++++++++ .../src/repository/form_repository_impl.rs | 16 ++++++- server/presentation/src/form_handler.rs | 14 ++++++ server/usecase/src/form.rs | 6 ++- 7 files changed, 88 insertions(+), 5 deletions(-) diff --git a/server/domain/src/repository/form_repository.rs b/server/domain/src/repository/form_repository.rs index aeeed3f2..6a2045df 100644 --- a/server/domain/src/repository/form_repository.rs +++ b/server/domain/src/repository/form_repository.rs @@ -5,7 +5,7 @@ use mockall::automock; use crate::{ form::models::{ Form, FormDescription, FormId, FormQuestionUpdateSchema, FormTitle, FormUpdateTargets, - OffsetAndLimit, PostedAnswers, SimpleForm, + OffsetAndLimit, PostedAnswers, Question, SimpleForm, }, user::models::User, }; @@ -30,4 +30,5 @@ pub trait FormRepository: Send + Sync + 'static { async fn post_answer(&self, answers: PostedAnswers) -> Result<(), Error>; async fn get_all_answers(&self) -> Result, Error>; async fn create_questions(&self, questions: FormQuestionUpdateSchema) -> Result<(), Error>; + async fn get_questions(&self, form_id: FormId) -> Result, Error>; } diff --git a/server/entrypoint/src/main.rs b/server/entrypoint/src/main.rs index ce9af3d5..4fab93bf 100644 --- a/server/entrypoint/src/main.rs +++ b/server/entrypoint/src/main.rs @@ -12,7 +12,8 @@ use presentation::{ auth::auth, form_handler::{ create_form_handler, create_question_handler, delete_form_handler, form_list_handler, - get_all_answers, get_form_handler, post_answer_handler, update_form_handler, + get_all_answers, get_form_handler, get_questions_handler, post_answer_handler, + update_form_handler, }, health_check_handler::health_check, }; @@ -69,6 +70,8 @@ async fn main() -> anyhow::Result<()> { .patch(update_form_handler), ) .with_state(shared_repository.to_owned()) + .route("/forms/:id/questions", get(get_questions_handler)) + .with_state(shared_repository.to_owned()) .route( "/forms/answers", post(post_answer_handler).get(get_all_answers), diff --git a/server/infra/resource/src/database/components.rs b/server/infra/resource/src/database/components.rs index 1a73dac1..e8aef3ac 100644 --- a/server/infra/resource/src/database/components.rs +++ b/server/infra/resource/src/database/components.rs @@ -9,7 +9,7 @@ use domain::{ use errors::infra::InfraError; use mockall::automock; -use crate::dto::{FormDto, PostedAnswersDto, SimpleFormDto}; +use crate::dto::{FormDto, PostedAnswersDto, QuestionDto, SimpleFormDto}; #[async_trait] pub trait DatabaseComponents: Send + Sync { @@ -46,6 +46,7 @@ pub trait FormDatabase: Send + Sync { async fn get_all_answers(&self) -> Result, InfraError>; async fn create_questions(&self, questions: FormQuestionUpdateSchema) -> Result<(), InfraError>; + async fn get_questions(&self, form_id: FormId) -> Result, InfraError>; } #[automock] diff --git a/server/infra/resource/src/database/form.rs b/server/infra/resource/src/database/form.rs index 68fcda31..81945587 100644 --- a/server/infra/resource/src/database/form.rs +++ b/server/infra/resource/src/database/form.rs @@ -408,4 +408,50 @@ impl FormDatabase for ConnectionPool { Ok(()) } + + async fn get_questions(&self, form_id: FormId) -> Result, InfraError> { + let questions_rs = self.query_all_and_values( + r"SELECT question_id, title, description, question_type, is_required FROM form_questions WHERE form_id = ?", + [form_id.to_owned().into()] + ).await?; + + let choices_rs = self + .query_all_and_values( + r"SELECT question_id, choice FROM form_choices + INNER JOIN form_questions ON form_choices.question_id = form_questions.question_id + WHERE form_id = ?", + [form_id.to_owned().into()], + ) + .await?; + + questions_rs + .into_iter() + .map(|question_rs| { + let question_id: i32 = question_rs.try_get("", "question_id")?; + + let choices = choices_rs + .iter() + .filter_map(|choice_rs| { + if choice_rs + .try_get::("", "question_id") + .is_ok_and(|id| id == question_id) + { + choice_rs.try_get::("", "choice").ok() + } else { + None + } + }) + .collect_vec(); + + Ok(QuestionDto { + id: question_id, + title: question_rs.try_get("", "title")?, + description: question_rs.try_get("", "description")?, + question_type: question_rs.try_get("", "question_type")?, + choices, + is_required: question_rs.try_get("", "is_required")?, + }) + }) + .collect::, _>>() + } } diff --git a/server/infra/resource/src/repository/form_repository_impl.rs b/server/infra/resource/src/repository/form_repository_impl.rs index 3fb96a79..1cae714b 100644 --- a/server/infra/resource/src/repository/form_repository_impl.rs +++ b/server/infra/resource/src/repository/form_repository_impl.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use domain::{ form::models::{ Form, FormDescription, FormId, FormQuestionUpdateSchema, FormTitle, FormUpdateTargets, - OffsetAndLimit, PostedAnswers, SimpleForm, + OffsetAndLimit, PostedAnswers, Question, SimpleForm, }, repository::form_repository::FormRepository, user::models::User, @@ -97,4 +97,18 @@ impl FormRepository for Repository .await .map_err(Into::into) } + + async fn get_questions(&self, form_id: FormId) -> Result, Error> { + self.client + .form() + .get_questions(form_id) + .await + .map(|questions_dto| { + questions_dto + .into_iter() + .map(|question_dto| question_dto.try_into()) + .collect::, _>>() + })? + .map_err(Into::into) + } } diff --git a/server/presentation/src/form_handler.rs b/server/presentation/src/form_handler.rs index 0733720a..c05c12a4 100644 --- a/server/presentation/src/form_handler.rs +++ b/server/presentation/src/form_handler.rs @@ -120,6 +120,20 @@ pub async fn update_form_handler( } } +pub async fn get_questions_handler( + State(repository): State, + Path(form_id): Path, +) -> impl IntoResponse { + let form_use_case = FormUseCase { + repository: repository.form_repository(), + }; + + match form_use_case.get_questions(form_id).await { + Ok(questions) => (StatusCode::OK, Json(questions)).into_response(), + Err(err) => handle_error(err).into_response(), + } +} + pub async fn get_all_answers( State(repository): State, ) -> impl IntoResponse { diff --git a/server/usecase/src/form.rs b/server/usecase/src/form.rs index 9877af57..db69c3a4 100644 --- a/server/usecase/src/form.rs +++ b/server/usecase/src/form.rs @@ -1,7 +1,7 @@ use domain::{ form::models::{ Form, FormDescription, FormId, FormQuestionUpdateSchema, FormTitle, FormUpdateTargets, - OffsetAndLimit, PostedAnswers, SimpleForm, + OffsetAndLimit, PostedAnswers, Question, SimpleForm, }, repository::form_repository::FormRepository, user::models::User, @@ -37,6 +37,10 @@ impl FormUseCase<'_, R> { self.repository.delete(form_id).await } + pub async fn get_questions(&self, form_id: FormId) -> Result, Error> { + self.repository.get_questions(form_id).await + } + pub async fn update_form( &self, form_id: FormId, From 3ee583962988c11980416080ae2ef0a8c8272fd3 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Sat, 28 Oct 2023 01:47:54 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20/forms/:id/questions=E3=81=AE?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=92=E4=B8=80=E8=88=AC=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=81=8C=E4=BD=BF=E3=81=88=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/Cargo.lock | 1 + server/domain/src/user/models.rs | 1 + server/infra/resource/src/database/form.rs | 4 ++-- server/presentation/Cargo.toml | 1 + server/presentation/src/auth.rs | 19 +++++++++++++++++-- 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index 006ddf14..17ac0f8d 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -2093,6 +2093,7 @@ dependencies = [ "common", "domain", "errors", + "regex", "reqwest", "resource", "serde", diff --git a/server/domain/src/user/models.rs b/server/domain/src/user/models.rs index c0167029..70c4ee53 100644 --- a/server/domain/src/user/models.rs +++ b/server/domain/src/user/models.rs @@ -14,5 +14,6 @@ pub struct User { pub enum Role { Administrator, #[default] + #[strum(serialize = "STANDARD_USER")] StandardUser, } diff --git a/server/infra/resource/src/database/form.rs b/server/infra/resource/src/database/form.rs index 81945587..fa146c67 100644 --- a/server/infra/resource/src/database/form.rs +++ b/server/infra/resource/src/database/form.rs @@ -417,7 +417,7 @@ impl FormDatabase for ConnectionPool { let choices_rs = self .query_all_and_values( - r"SELECT question_id, choice FROM form_choices + r"SELECT form_choices.question_id AS choice_question_id, choice FROM form_choices INNER JOIN form_questions ON form_choices.question_id = form_questions.question_id WHERE form_id = ?", [form_id.to_owned().into()], @@ -427,7 +427,7 @@ impl FormDatabase for ConnectionPool { questions_rs .into_iter() .map(|question_rs| { - let question_id: i32 = question_rs.try_get("", "question_id")?; + let question_id: i32 = question_rs.try_get("", "choice_question_id")?; let choices = choices_rs .iter() diff --git a/server/presentation/Cargo.toml b/server/presentation/Cargo.toml index 99b1e1f8..aad30e40 100644 --- a/server/presentation/Cargo.toml +++ b/server/presentation/Cargo.toml @@ -15,3 +15,4 @@ tracing = { workspace = true } usecase = { path = "../usecase" } uuid = { workspace = true } reqwest = { workspace = true } +regex = { workspace = true } diff --git a/server/presentation/src/auth.rs b/server/presentation/src/auth.rs index d3408f02..602bc058 100644 --- a/server/presentation/src/auth.rs +++ b/server/presentation/src/auth.rs @@ -13,6 +13,7 @@ use domain::{ User, }, }; +use regex::Regex; use reqwest::header::{ACCEPT, CONTENT_TYPE}; use resource::repository::RealInfrastructureRepository; use usecase::user::UserUseCase; @@ -54,10 +55,24 @@ pub async fn auth( .map_err(|_| StatusCode::UNAUTHORIZED)? }; - let standard_user_endpoints = [(&Method::GET, "/forms"), (&Method::POST, "/forms/answers")]; + let static_endpoints_allowed_for_standard_users = + [(&Method::GET, "/forms"), (&Method::POST, "/forms/answers")]; + + // NOTE: 動的パスを指定する場合は、正規表現を埋め込む + let dynamic_endpoints_allowed_for_standard_users = [(&Method::GET, "/forms/[^/]+/questions")]; + + let is_not_allow_dynamic_endpoint = !dynamic_endpoints_allowed_for_standard_users + .into_iter() + .any(|(method, endpoint)| { + let regex = Regex::new(endpoint).unwrap(); + + method == request.method() && regex.is_match(request.uri().path()) + }); if user.role == StandardUser - && !standard_user_endpoints.contains(&(request.method(), request.uri().path())) + && !static_endpoints_allowed_for_standard_users + .contains(&(request.method(), request.uri().path())) + && is_not_allow_dynamic_endpoint { // NOTE: standard_user_endpointsに存在しないMethodとエンドポイントに // 一般ユーザーがアクセスした場合は、アクセス権限なしとしてすべてFORBIDDENを返す。 From 99f06e6377761a8d74b4389e2e5f640337c48099 Mon Sep 17 00:00:00 2001 From: rito528 <39003544+rito528@users.noreply.github.com> Date: Tue, 31 Oct 2023 23:57:48 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E6=8C=87=E5=AE=9A=E3=82=AB=E3=83=A9?= =?UTF-8?q?=E3=83=A0=E5=90=8D=E3=81=AE=E9=96=93=E9=81=95=E3=81=84=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/infra/resource/src/database/form.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/infra/resource/src/database/form.rs b/server/infra/resource/src/database/form.rs index fa146c67..a108abf1 100644 --- a/server/infra/resource/src/database/form.rs +++ b/server/infra/resource/src/database/form.rs @@ -417,7 +417,7 @@ impl FormDatabase for ConnectionPool { let choices_rs = self .query_all_and_values( - r"SELECT form_choices.question_id AS choice_question_id, choice FROM form_choices + r"SELECT form_choices.question_id, choice FROM form_choices INNER JOIN form_questions ON form_choices.question_id = form_questions.question_id WHERE form_id = ?", [form_id.to_owned().into()], @@ -427,7 +427,7 @@ impl FormDatabase for ConnectionPool { questions_rs .into_iter() .map(|question_rs| { - let question_id: i32 = question_rs.try_get("", "choice_question_id")?; + let question_id: i32 = question_rs.try_get("", "question_id")?; let choices = choices_rs .iter()