headless_lms_server/controllers/tmc_server/
users_by_upstream_id.rs1use crate::{
8 domain::authentication::{
9 authenticate_tmc_server, get_or_create_user_from_tmc_mooc_fi_response,
10 },
11 prelude::*,
12};
13use headless_lms_utils::services::tmc::TmcClient;
14use models::users::User;
15
16#[instrument(skip(pool))]
22pub async fn get_user_by_upstream_id(
23 upstream_id: web::Path<i32>,
24 pool: web::Data<PgPool>,
25 request: HttpRequest,
26 tmc_client: web::Data<TmcClient>,
27) -> ControllerResult<web::Json<User>> {
28 let mut conn = pool.acquire().await?;
29 let token = authenticate_tmc_server(&request).await?;
30 let tmc_user = tmc_client
31 .get_user_from_tmc_mooc_fi_by_tmc_access_token_and_upstream_id(&upstream_id)
32 .await?;
33
34 debug!(
35 "Creating or fetching user with TMC id {} and mooc.fi UUID {}",
36 tmc_user.id,
37 tmc_user
38 .courses_mooc_fi_user_id
39 .map(|uuid| uuid.to_string())
40 .unwrap_or_else(|| "None (will generate new UUID)".to_string())
41 );
42 let user = get_or_create_user_from_tmc_mooc_fi_response(
43 &mut conn,
44 tmc_user,
45 tmc_client.get_admin_access_token(),
46 )
47 .await?;
48 info!(
49 "Successfully got user details from mooc.fi for user {}",
50 user.id
51 );
52
53 token.authorized_ok(web::Json(user))
54}
55
56#[derive(Debug, Serialize)]
57pub struct UserMigrationStatusResponse {
58 pub shadow_user_exists: bool,
59 pub courses_mooc_fi_user_id: Option<Uuid>,
60 pub password_set: bool,
61 pub deleted_at: Option<DateTime<Utc>>,
62}
63
64#[instrument(skip(pool))]
73pub async fn get_migration_status_by_upstream_id(
74 upstream_id: web::Path<i32>,
75 pool: web::Data<PgPool>,
76 request: HttpRequest,
77) -> ControllerResult<web::Json<UserMigrationStatusResponse>> {
78 let token = authenticate_tmc_server(&request).await?;
79 let mut conn = pool.acquire().await?;
80
81 let response = match models::users::find_by_upstream_id(&mut conn, *upstream_id).await? {
82 Some(user) => {
83 let password_set =
84 models::user_passwords::check_if_users_password_is_stored(&mut conn, user.id)
85 .await?;
86 UserMigrationStatusResponse {
87 shadow_user_exists: true,
88 courses_mooc_fi_user_id: Some(user.id),
89 password_set,
90 deleted_at: user.deleted_at,
91 }
92 }
93 None => UserMigrationStatusResponse {
94 shadow_user_exists: false,
95 courses_mooc_fi_user_id: None,
96 password_set: false,
97 deleted_at: None,
98 },
99 };
100
101 token.authorized_ok(web::Json(response))
102}
103
104pub fn _add_routes(cfg: &mut ServiceConfig) {
105 cfg.route("/{user_id}", web::get().to(get_user_by_upstream_id))
106 .route(
107 "/{user_id}/status",
108 web::get().to(get_migration_status_by_upstream_id),
109 );
110}
111
112#[cfg(test)]
113mod test {
114 use super::*;
115 use crate::test_helper::*;
116 use models::user_passwords;
117 use models::users;
118 use secrecy::SecretString;
119
120 #[actix_web::test]
121 async fn migration_status_reflects_shadow_user_and_password_state() {
122 let mut conn = Conn::init().await;
123 let mut tx = conn.begin().await;
124
125 let upstream_id = 987_654_321;
126 let moocfi_id = Uuid::new_v4();
127
128 assert!(
129 users::find_by_upstream_id(tx.as_mut(), upstream_id)
130 .await
131 .unwrap()
132 .is_none()
133 );
134
135 let user = users::insert_with_upstream_id_and_moocfi_id(
136 tx.as_mut(),
137 "migration-status-test@example.com",
138 None,
139 None,
140 upstream_id,
141 moocfi_id,
142 )
143 .await
144 .unwrap();
145
146 let found = users::find_by_upstream_id(tx.as_mut(), upstream_id)
147 .await
148 .unwrap()
149 .unwrap();
150 assert_eq!(found.id, user.id);
151 assert!(
152 !user_passwords::check_if_users_password_is_stored(tx.as_mut(), user.id)
153 .await
154 .unwrap()
155 );
156
157 let hash =
158 user_passwords::hash_password(&SecretString::new("test-password".to_string().into()))
159 .unwrap();
160 user_passwords::upsert_user_password(tx.as_mut(), user.id, &hash)
161 .await
162 .unwrap();
163
164 assert!(
165 user_passwords::check_if_users_password_is_stored(tx.as_mut(), user.id)
166 .await
167 .unwrap()
168 );
169 }
170}