headless_lms_server/controllers/main_frontend/
user_details.rs1use models::{pages::SearchRequest, user_details::UserDetail};
2
3use crate::{controllers, prelude::*};
4use headless_lms_utils::{ip_to_country::IpToCountryMapper, tmc::TmcClient};
5use std::net::IpAddr;
6
7#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
8#[cfg_attr(feature = "ts_rs", derive(TS))]
9pub struct BulkUserDetailsRequest {
10 pub user_ids: Vec<Uuid>,
11 pub course_id: Uuid,
12}
13
14#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
15#[cfg_attr(feature = "ts_rs", derive(TS))]
16pub struct UserDetailsRequest {
17 pub user_id: Uuid,
18 pub course_ids: Vec<Uuid>,
19}
20
21#[instrument(skip(pool))]
26pub async fn get_user_details(
27 user: AuthUser,
28 pool: web::Data<PgPool>,
29 path: web::Path<(Uuid, Uuid)>,
30) -> ControllerResult<web::Json<UserDetail>> {
31 let (course_id, user_id) = path.into_inner();
32 let mut conn = pool.acquire().await?;
33
34 let token = authorize(
35 &mut conn,
36 Act::ViewUserProgressOrDetails,
37 Some(user.id),
38 Res::Course(course_id),
39 )
40 .await?;
41 let res =
42 models::user_details::get_user_details_by_user_id_for_course(&mut conn, user_id, course_id)
43 .await?;
44 token.authorized_ok(web::Json(res))
45}
46
47#[instrument(skip(pool))]
52pub async fn get_user_details_by_courses(
53 user: AuthUser,
54 pool: web::Data<PgPool>,
55 payload: web::Json<UserDetailsRequest>,
56) -> ControllerResult<web::Json<UserDetail>> {
57 let mut conn = pool.acquire().await?;
58
59 let mut token = None;
61
62 if payload.course_ids.is_empty() {
63 token = Some(
65 authorize(
66 &mut conn,
67 Act::ViewUserProgressOrDetails,
68 Some(user.id),
69 Res::GlobalPermissions,
70 )
71 .await?,
72 );
73 } else {
74 for course_id in &payload.course_ids {
75 if let Ok(auth_token) = authorize(
76 &mut conn,
77 Act::ViewUserProgressOrDetails,
78 Some(user.id),
79 Res::Course(*course_id),
80 )
81 .await
82 {
83 token = Some(auth_token);
84 break;
85 }
86 }
87 }
88
89 let token = token.ok_or_else(|| {
90 ControllerError::new(
91 ControllerErrorType::Forbidden,
92 "No permission to view user details through any of the provided courses".to_string(),
93 None,
94 )
95 })?;
96
97 let res = models::user_details::get_user_details_by_user_id(&mut conn, payload.user_id).await?;
99 token.authorized_ok(web::Json(res))
100}
101
102#[instrument(skip(pool))]
106pub async fn search_users_by_email(
107 user: AuthUser,
108 pool: web::Data<PgPool>,
109 payload: web::Json<SearchRequest>,
110) -> ControllerResult<web::Json<Vec<UserDetail>>> {
111 let mut conn = pool.acquire().await?;
112
113 let token = authorize(
114 &mut conn,
115 Act::ViewUserProgressOrDetails,
116 Some(user.id),
117 Res::GlobalPermissions,
118 )
119 .await?;
120 let res =
121 models::user_details::search_for_user_details_by_email(&mut conn, &payload.query).await?;
122 token.authorized_ok(web::Json(res))
123}
124
125#[instrument(skip(pool))]
129pub async fn search_users_by_other_details(
130 user: AuthUser,
131 pool: web::Data<PgPool>,
132 payload: web::Json<SearchRequest>,
133) -> ControllerResult<web::Json<Vec<UserDetail>>> {
134 let mut conn = pool.acquire().await?;
135
136 let token = authorize(
137 &mut conn,
138 Act::ViewUserProgressOrDetails,
139 Some(user.id),
140 Res::GlobalPermissions,
141 )
142 .await?;
143 let res =
144 models::user_details::search_for_user_details_by_other_details(&mut conn, &payload.query)
145 .await?;
146 token.authorized_ok(web::Json(res))
147}
148
149#[instrument(skip(pool))]
153pub async fn search_users_fuzzy_match(
154 user: AuthUser,
155 pool: web::Data<PgPool>,
156 payload: web::Json<SearchRequest>,
157) -> ControllerResult<web::Json<Vec<UserDetail>>> {
158 let mut conn = pool.acquire().await?;
159
160 let token = authorize(
161 &mut conn,
162 Act::ViewUserProgressOrDetails,
163 Some(user.id),
164 Res::GlobalPermissions,
165 )
166 .await?;
167 let res = models::user_details::search_for_user_details_fuzzy_match(&mut conn, &payload.query)
168 .await?;
169 token.authorized_ok(web::Json(res))
170}
171
172pub async fn get_users_by_course_id(
176 course_id: web::Path<Uuid>,
177 user: AuthUser,
178 pool: web::Data<PgPool>,
179) -> ControllerResult<web::Json<Vec<UserDetail>>> {
180 let mut conn = pool.acquire().await?;
181
182 let token = authorize(
183 &mut conn,
184 Act::ViewUserProgressOrDetails,
185 Some(user.id),
186 Res::Course(*course_id),
187 )
188 .await?;
189 let res = models::user_details::get_users_by_course_id(&mut conn, *course_id).await?;
190 token.authorized_ok(web::Json(res))
191}
192
193#[instrument(skip(pool))]
198pub async fn get_bulk_user_details(
199 user: AuthUser,
200 pool: web::Data<PgPool>,
201 payload: web::Json<BulkUserDetailsRequest>,
202) -> ControllerResult<web::Json<Vec<UserDetail>>> {
203 let mut conn = pool.acquire().await?;
204
205 let token = authorize(
206 &mut conn,
207 Act::ViewUserProgressOrDetails,
208 Some(user.id),
209 Res::Course(payload.course_id),
210 )
211 .await?;
212 let res = models::user_details::get_user_details_by_user_ids_for_course(
213 &mut conn,
214 &payload.user_ids,
215 payload.course_id,
216 )
217 .await?;
218 token.authorized_ok(web::Json(res))
219}
220
221#[instrument(skip(pool))]
225pub async fn get_user_details_for_user(
226 user: AuthUser,
227 pool: web::Data<PgPool>,
228) -> ControllerResult<web::Json<UserDetail>> {
229 let mut conn = pool.acquire().await?;
230
231 let token = skip_authorize();
232 let user_id = user.id;
233 let res = models::user_details::get_user_details_by_user_id(&mut conn, user_id).await?;
234 token.authorized_ok(web::Json(res))
235}
236
237pub async fn get_user_country_by_ip(
238 req: HttpRequest,
239 ip_to_country_mapper: web::Data<IpToCountryMapper>,
240) -> ControllerResult<String> {
241 let connection_info = req.connection_info();
242
243 let ip: Option<IpAddr> = connection_info
244 .realip_remote_addr()
245 .and_then(|ip| ip.parse::<IpAddr>().ok());
246
247 let country = ip
248 .and_then(|ip| ip_to_country_mapper.map_ip_to_country(&ip))
249 .map(|c| c.to_string())
250 .unwrap_or_default();
251
252 let token = skip_authorize();
253 token.authorized_ok(country)
254}
255
256#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
257#[cfg_attr(feature = "ts_rs", derive(TS))]
258pub struct UserInfoPayload {
259 pub email: String,
260 pub first_name: String,
261 pub last_name: String,
262 pub country: String,
263 pub email_communication_consent: bool,
264}
265
266#[instrument(skip(pool, app_conf, tmc_client))]
270pub async fn update_user_info(
271 user: AuthUser,
272 pool: web::Data<PgPool>,
273 payload: web::Json<UserInfoPayload>,
274 tmc_client: web::Data<TmcClient>,
275 app_conf: web::Data<ApplicationConfiguration>,
276) -> ControllerResult<web::Json<UserDetail>> {
277 let mut tx = pool.begin().await?;
278
279 let existing_user = models::user_details::get_user_details_by_user_id(&mut tx, user.id)
280 .await
281 .context("Failed to fetch existing user data")?;
282
283 let user = models::users::get_by_id(&mut tx, user.id)
284 .await
285 .context("Failed to fetch user")?;
286
287 let updated_user = models::user_details::update_user_info(
288 &mut tx,
289 user.id,
290 &payload.email,
291 &payload.first_name,
292 &payload.last_name,
293 &payload.country,
294 payload.email_communication_consent,
295 )
296 .await
297 .context("Failed to update database")?;
298
299 let email_changed = existing_user.email != payload.email;
300 let first_name_changed = existing_user.first_name != Some(payload.first_name.clone());
301 let last_name_changed = existing_user.last_name != Some(payload.last_name.clone());
302
303 if !app_conf.test_mode && (email_changed || first_name_changed || last_name_changed) {
304 let email_opt = if email_changed {
305 Some(payload.email.clone())
306 } else {
307 None
308 };
309
310 let upstream_id = user
311 .upstream_id
312 .ok_or_else(|| {
313 ControllerError::new(
314 ControllerErrorType::InternalServerError,
315 "Missing upstream_id".to_string(),
316 None,
317 )
318 })?
319 .to_string();
320
321 controllers::auth::update_user_information_to_tmc(
322 payload.first_name.clone(),
323 payload.last_name.clone(),
324 email_opt,
325 upstream_id,
326 tmc_client.clone(),
327 app_conf,
328 )
329 .await
330 .map_err(|e| {
331 ControllerError::new(
332 ControllerErrorType::InternalServerError,
333 "Failed to update user info to tmc",
334 e,
335 )
336 })?;
337 } else {
338 info!("User info unchanged, skipping update to TMC.");
339 }
340
341 tx.commit().await?;
342 let token = skip_authorize();
343 token.authorized_ok(web::Json(updated_user))
344}
345
346pub fn _add_routes(cfg: &mut ServiceConfig) {
347 cfg.route("/search-by-email", web::post().to(search_users_by_email))
348 .route(
349 "/search-by-other-details",
350 web::post().to(search_users_by_other_details),
351 )
352 .route(
353 "/search-fuzzy-match",
354 web::post().to(search_users_fuzzy_match),
355 )
356 .route(
357 "/{course_id}/user/{user_id}",
358 web::get().to(get_user_details),
359 )
360 .route(
361 "/user-by-courses",
362 web::post().to(get_user_details_by_courses),
363 )
364 .route("/users-ip-country", web::get().to(get_user_country_by_ip))
365 .route(
366 "/user-details-for-user",
367 web::get().to(get_user_details_for_user),
368 )
369 .route("/update-user-info", web::post().to(update_user_info))
370 .route(
371 "/{course_id}/get-users-by-course-id",
372 web::get().to(get_users_by_course_id),
373 )
374 .route("/bulk-user-details", web::post().to(get_bulk_user_details));
375}