feat(org): enforce tenant isolation in ws/kc with leave and stats APIs
Add org_id schema migrations and service-level filtering for teacher-student and class workflows, then cover org-context behavior with focused unit/e2e tests for leave and hour statistics. Made-with: Cursor
This commit is contained in:
+222
-66
@@ -10,6 +10,12 @@ use diesel::PgConnection;
|
||||
use reqwest::header::{HeaderValue, CONTENT_TYPE};
|
||||
use tracing::{debug, error};
|
||||
|
||||
fn current_org_id_from_token_str(token_str: &String) -> Option<String> {
|
||||
jwt_decode_token(token_str)
|
||||
.ok()
|
||||
.and_then(|decoded| decoded.current_org_id)
|
||||
}
|
||||
|
||||
use htycommons::common::{
|
||||
current_local_datetime, get_page_and_page_size, get_some_from_query_params, strip_result_vec,
|
||||
HtyErr, HtyErrCode, HtyResponse,
|
||||
@@ -2159,14 +2165,14 @@ pub async fn raw_update_course_section_by_id_with_tx(
|
||||
|
||||
pub async fn find_teacher_students_by_ids(
|
||||
Query(page_params): Query<HashMap<String, String>>,
|
||||
_sudoer: HtySudoerTokenHeader,
|
||||
sudoer: HtySudoerTokenHeader,
|
||||
State(db_pool): State<Arc<DbState>>,
|
||||
Json(params): Json<ReqTeacherStudentsQuery>,
|
||||
) -> Json<HtyResponse<(Vec<ReqTeacherStudent>, i64, i64)>> {
|
||||
debug!("find_teacher_students_by_ids -> start here");
|
||||
let (page, page_size) = get_page_and_page_size(&page_params);
|
||||
|
||||
match raw_find_teacher_students_by_ids(&page, &page_size, params, db_pool) {
|
||||
match raw_find_teacher_students_by_ids(&page, &page_size, params, &sudoer, db_pool) {
|
||||
Ok(all) => {
|
||||
debug!("raw_find_teacher_students_by_ids -> out: {:?}", all);
|
||||
wrap_json_ok_resp(all)
|
||||
@@ -2182,17 +2188,29 @@ pub fn raw_find_teacher_students_by_ids(
|
||||
page: &Option<i64>,
|
||||
page_size: &Option<i64>,
|
||||
params: ReqTeacherStudentsQuery,
|
||||
sudoer: &HtySudoerTokenHeader,
|
||||
db_pool: Arc<DbState>,
|
||||
) -> anyhow::Result<(Vec<ReqTeacherStudent>, i64, i64)> {
|
||||
let query_ids = params
|
||||
.hty_ids
|
||||
.ok_or_else(|| anyhow!("hty_ids is required"))?;
|
||||
let (records, total_page, total) = TeacherStudent::find_with_page_by_ids(
|
||||
page,
|
||||
page_size,
|
||||
&query_ids,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let current_org_id = current_org_id_from_token_str(&sudoer.0);
|
||||
let (records, total_page, total) = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::find_with_page_by_ids_in_org(
|
||||
page,
|
||||
page_size,
|
||||
&query_ids,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
TeacherStudent::find_with_page_by_ids(
|
||||
page,
|
||||
page_size,
|
||||
&query_ids,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
};
|
||||
let req_records = TeacherStudent::to_req_vec(&records);
|
||||
debug!(
|
||||
"raw_find_teacher_students_by_ids -> req_records: {:?}",
|
||||
@@ -2203,14 +2221,14 @@ pub fn raw_find_teacher_students_by_ids(
|
||||
|
||||
pub async fn find_all_teacher_students(
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
_root: HtySudoerTokenHeader,
|
||||
root: HtySudoerTokenHeader,
|
||||
State(db_pool): State<Arc<DbState>>,
|
||||
) -> Json<HtyResponse<(Vec<ReqTeacherStudent>, i64, i64)>> {
|
||||
debug!("find_all_teacher_students -> start here");
|
||||
|
||||
let (page, page_size) = get_page_and_page_size(¶ms);
|
||||
|
||||
match raw_find_all_teacher_students(&page, &page_size, db_pool) {
|
||||
match raw_find_all_teacher_students(&page, &page_size, &root, db_pool) {
|
||||
Ok(all) => {
|
||||
debug!("find_all_teacher_students -> out: {:?}", all);
|
||||
wrap_json_ok_resp(all)
|
||||
@@ -2225,13 +2243,24 @@ pub async fn find_all_teacher_students(
|
||||
pub fn raw_find_all_teacher_students(
|
||||
page: &Option<i64>,
|
||||
page_size: &Option<i64>,
|
||||
root: &HtySudoerTokenHeader,
|
||||
db_pool: Arc<DbState>,
|
||||
) -> anyhow::Result<(Vec<ReqTeacherStudent>, i64, i64)> {
|
||||
let (records, total_page, total) = TeacherStudent::find_all_with_page(
|
||||
page,
|
||||
page_size,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let current_org_id = current_org_id_from_token_str(&root.0);
|
||||
let (records, total_page, total) = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::find_all_with_page_in_org(
|
||||
page,
|
||||
page_size,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
TeacherStudent::find_all_with_page(
|
||||
page,
|
||||
page_size,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
};
|
||||
let req_records = TeacherStudent::to_req_vec(&records);
|
||||
Ok((req_records, total_page, total))
|
||||
}
|
||||
@@ -3167,7 +3196,7 @@ pub async fn claim_student(
|
||||
}
|
||||
|
||||
pub async fn raw_claim_student(
|
||||
_token: AuthorizationHeader,
|
||||
token: AuthorizationHeader,
|
||||
some_id_teacher: Option<String>,
|
||||
some_id_student: Option<String>,
|
||||
db_pool: Arc<DbState>,
|
||||
@@ -3188,11 +3217,21 @@ pub async fn raw_claim_student(
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("id_student is required"))?;
|
||||
|
||||
let res_exist = TeacherStudent::verify_exist_by_teacher_id_and_student_id(
|
||||
&id_teacher,
|
||||
&id_student,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let current_org_id = current_org_id_from_token_str(&(*token).clone());
|
||||
let res_exist = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::verify_exist_by_teacher_id_and_student_id_in_org(
|
||||
&id_teacher,
|
||||
&id_student,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
TeacherStudent::verify_exist_by_teacher_id_and_student_id(
|
||||
&id_teacher,
|
||||
&id_student,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
};
|
||||
if res_exist {
|
||||
return Err(anyhow!(HtyErr {
|
||||
code: HtyErrCode::WebErr,
|
||||
@@ -3205,6 +3244,7 @@ pub async fn raw_claim_student(
|
||||
id: uuid(),
|
||||
status: String::from("Approved"),
|
||||
created_at: Some(current_local_datetime()),
|
||||
org_id: current_org_id,
|
||||
};
|
||||
let res = TeacherStudent::create(
|
||||
&in_record,
|
||||
@@ -3214,12 +3254,13 @@ pub async fn raw_claim_student(
|
||||
}
|
||||
|
||||
pub async fn link_teacher_student(
|
||||
auth: AuthorizationHeader,
|
||||
State(db_pool): State<Arc<DbState>>,
|
||||
link_data: Json<ReqTeacherStudent>,
|
||||
) -> Json<HtyResponse<String>> {
|
||||
debug!("link_teacher_student -> starts");
|
||||
let input = link_data.0;
|
||||
match raw_link_teacher_student(input, db_pool).await {
|
||||
match raw_link_teacher_student(auth, input, db_pool).await {
|
||||
Ok(result) => wrap_json_ok_resp(result),
|
||||
Err(e) => {
|
||||
error!(
|
||||
@@ -3232,9 +3273,12 @@ pub async fn link_teacher_student(
|
||||
}
|
||||
|
||||
pub async fn raw_link_teacher_student(
|
||||
auth: AuthorizationHeader,
|
||||
link_data: ReqTeacherStudent,
|
||||
db_pool: Arc<DbState>,
|
||||
) -> anyhow::Result<String> {
|
||||
let current_org_id = current_org_id_from_token_str(&(*auth).clone())
|
||||
.ok_or_else(|| anyhow!("current_org_id is required"))?;
|
||||
let teacher_student = TeacherStudent {
|
||||
teacher_id: link_data
|
||||
.teacher_id
|
||||
@@ -3245,6 +3289,7 @@ pub async fn raw_link_teacher_student(
|
||||
id: uuid(),
|
||||
status: link_data.status.unwrap_or(String::from("Waiting")),
|
||||
created_at: Some(current_local_datetime()),
|
||||
org_id: Some(current_org_id),
|
||||
};
|
||||
let res = TeacherStudent::create(
|
||||
&teacher_student,
|
||||
@@ -3274,7 +3319,7 @@ pub async fn disclaim_student(
|
||||
}
|
||||
|
||||
pub async fn raw_disclaim_student(
|
||||
_auth: AuthorizationHeader,
|
||||
auth: AuthorizationHeader,
|
||||
some_id_teacher: Option<String>,
|
||||
some_id_student: Option<String>,
|
||||
db_pool: Arc<DbState>,
|
||||
@@ -3295,26 +3340,42 @@ pub async fn raw_disclaim_student(
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("id_student is required"))?;
|
||||
|
||||
let res_exist = TeacherStudent::verify_exist_by_teacher_id_and_student_id(
|
||||
&id_teacher,
|
||||
&id_student,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let current_org_id = current_org_id_from_token_str(&(*auth).clone());
|
||||
let res_exist = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::verify_exist_by_teacher_id_and_student_id_in_org(
|
||||
&id_teacher,
|
||||
&id_student,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
TeacherStudent::verify_exist_by_teacher_id_and_student_id(
|
||||
&id_teacher,
|
||||
&id_student,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
};
|
||||
if !res_exist {
|
||||
return Err(anyhow!(HtyErr {
|
||||
code: HtyErrCode::WebErr,
|
||||
reason: Some("no existing record for this teacher and student".into()),
|
||||
}));
|
||||
}
|
||||
let record = TeacherStudent::find_by_teacher_id_and_student_id(
|
||||
&id_teacher,
|
||||
&id_student,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let res = TeacherStudent::delete_by_id(
|
||||
&record.id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let res = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::del_by_teacher_id_and_student_id_in_org(
|
||||
&id_teacher,
|
||||
&id_student,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
let record = TeacherStudent::find_by_teacher_id_and_student_id(
|
||||
&id_teacher,
|
||||
&id_student,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
TeacherStudent::delete_by_id(&record.id, extract_conn(fetch_db_conn(&db_pool)?).deref_mut())?
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -3518,10 +3579,19 @@ pub async fn raw_find_all_teachers_by_student_id(
|
||||
student_id: String,
|
||||
db_pool: Arc<DbState>,
|
||||
) -> anyhow::Result<Vec<ReqHtyUserWithInfos>> {
|
||||
let teacher_ids = TeacherStudent::get_all_teachers_by_student_id(
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let current_org_id = current_org_id_from_token_str(&root.0);
|
||||
let teacher_ids = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::get_all_teachers_by_student_id_in_org(
|
||||
&student_id,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
TeacherStudent::get_all_teachers_by_student_id(
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
};
|
||||
debug!(
|
||||
"raw_find_all_teachers_by_student_id -> teacher_ids {:?}",
|
||||
&teacher_ids
|
||||
@@ -3582,11 +3652,21 @@ pub async fn raw_delete_teacher_student(
|
||||
let _teacher = find_hty_user_with_info_by_id(&teacher_id, &root).await?;
|
||||
let _student = find_hty_user_with_info_by_id(&student_id, &root).await?;
|
||||
|
||||
let is_exist = TeacherStudent::verify_exist_by_teacher_id_and_student_id(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let current_org_id = current_org_id_from_token_str(&root.0);
|
||||
let is_exist = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::verify_exist_by_teacher_id_and_student_id_in_org(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
TeacherStudent::verify_exist_by_teacher_id_and_student_id(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
};
|
||||
|
||||
if !is_exist {
|
||||
return Err(anyhow!(HtyErr {
|
||||
@@ -3602,11 +3682,20 @@ pub async fn raw_delete_teacher_student(
|
||||
// )?;
|
||||
|
||||
// record.status = status;
|
||||
let _ = TeacherStudent::del_by_teacher_id_and_student_id(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
if let Some(org_id) = ¤t_org_id {
|
||||
let _ = TeacherStudent::del_by_teacher_id_and_student_id_in_org(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
} else {
|
||||
let _ = TeacherStudent::del_by_teacher_id_and_student_id(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3664,22 +3753,41 @@ pub async fn raw_update_teacher_student(
|
||||
let _teacher = find_hty_user_with_info_by_id(&teacher_id, &root).await?;
|
||||
let _student = find_hty_user_with_info_by_id(&student_id, &root).await?;
|
||||
|
||||
let is_exist = TeacherStudent::verify_exist_by_teacher_id_and_student_id(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let current_org_id = current_org_id_from_token_str(&root.0);
|
||||
let is_exist = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::verify_exist_by_teacher_id_and_student_id_in_org(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
TeacherStudent::verify_exist_by_teacher_id_and_student_id(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
};
|
||||
if !is_exist {
|
||||
return Err(anyhow!(HtyErr {
|
||||
code: HtyErrCode::NullErr,
|
||||
reason: Some("no record for this teacher and student".to_string()),
|
||||
}));
|
||||
}
|
||||
let mut record = TeacherStudent::find_by_teacher_id_and_student_id(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let mut record = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::find_by_teacher_id_and_student_id_in_org(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
TeacherStudent::find_by_teacher_id_and_student_id(
|
||||
&teacher_id,
|
||||
&student_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
};
|
||||
record.status = status;
|
||||
let _ = TeacherStudent::update(&record, extract_conn(fetch_db_conn(&db_pool)?).deref_mut())?;
|
||||
Ok(())
|
||||
@@ -3726,11 +3834,21 @@ pub async fn raw_find_all_students_by_teacher_id_and_status(
|
||||
let status = req_teacher_student
|
||||
.status
|
||||
.ok_or_else(|| anyhow!("status is required"))?;
|
||||
let student_ids = TeacherStudent::get_all_students_by_teacher_id_and_status(
|
||||
&teacher_id,
|
||||
&status,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?;
|
||||
let current_org_id = current_org_id_from_token_str(&root.0);
|
||||
let student_ids = if let Some(org_id) = ¤t_org_id {
|
||||
TeacherStudent::get_all_students_by_teacher_id_and_status_in_org(
|
||||
&teacher_id,
|
||||
&status,
|
||||
org_id,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
} else {
|
||||
TeacherStudent::get_all_students_by_teacher_id_and_status(
|
||||
&teacher_id,
|
||||
&status,
|
||||
extract_conn(fetch_db_conn(&db_pool)?).deref_mut(),
|
||||
)?
|
||||
};
|
||||
debug!(
|
||||
"raw_find_all_students_by_teacher_id_and_status -> student_ids {:?}",
|
||||
&student_ids
|
||||
@@ -4173,3 +4291,41 @@ pub async fn raw_find_all_course_sections_by_created_by_with_page(
|
||||
|
||||
Ok((result, total_page, total))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use htycommons::common::current_local_datetime;
|
||||
use htycommons::jwt::jwt_encode_token;
|
||||
use htycommons::web::HtyToken;
|
||||
|
||||
use super::current_org_id_from_token_str;
|
||||
|
||||
fn build_test_token(current_org_id: Option<&str>) -> String {
|
||||
std::env::set_var("JWT_KEY", "htyws-org-test-key");
|
||||
let token = HtyToken {
|
||||
token_id: "test-token-id".to_string(),
|
||||
hty_id: Some("teacher-1".to_string()),
|
||||
app_id: Some("app-1".to_string()),
|
||||
ts: current_local_datetime(),
|
||||
roles: None,
|
||||
tags: None,
|
||||
current_org_id: current_org_id.map(|value| value.to_string()),
|
||||
current_org_role_keys: Some(vec!["TEACHER".to_string()]),
|
||||
};
|
||||
jwt_encode_token(token).expect("encode test token")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_extract_current_org_id_from_valid_token() {
|
||||
let raw_token = build_test_token(Some("org-123"));
|
||||
let parsed_org_id = current_org_id_from_token_str(&raw_token);
|
||||
assert_eq!(parsed_org_id, Some("org-123".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_none_when_token_has_no_current_org_id() {
|
||||
let raw_token = build_test_token(None);
|
||||
let parsed_org_id = current_org_id_from_token_str(&raw_token);
|
||||
assert_eq!(parsed_org_id, None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user