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#[instrument(skip(pool))]
19pub async fn get_user_details(
20 user: AuthUser,
21 pool: web::Data<PgPool>,
22 path: web::Path<(Uuid, Uuid)>,
23) -> ControllerResult<web::Json<UserDetail>> {
24 let (course_id, user_id) = path.into_inner();
25 let mut conn = pool.acquire().await?;
26
27 let token = authorize(
28 &mut conn,
29 Act::ViewUserProgressOrDetails,
30 Some(user.id),
31 Res::Course(course_id),
32 )
33 .await?;
34 let res =
35 models::user_details::get_user_details_by_user_id_for_course(&mut conn, user_id, course_id)
36 .await?;
37 token.authorized_ok(web::Json(res))
38}
39
40#[instrument(skip(pool))]
44pub async fn search_users_by_email(
45 user: AuthUser,
46 pool: web::Data<PgPool>,
47 payload: web::Json<SearchRequest>,
48) -> ControllerResult<web::Json<Vec<UserDetail>>> {
49 let mut conn = pool.acquire().await?;
50
51 let token = authorize(
52 &mut conn,
53 Act::ViewUserProgressOrDetails,
54 Some(user.id),
55 Res::GlobalPermissions,
56 )
57 .await?;
58 let res =
59 models::user_details::search_for_user_details_by_email(&mut conn, &payload.query).await?;
60 token.authorized_ok(web::Json(res))
61}
62
63#[instrument(skip(pool))]
67pub async fn search_users_by_other_details(
68 user: AuthUser,
69 pool: web::Data<PgPool>,
70 payload: web::Json<SearchRequest>,
71) -> ControllerResult<web::Json<Vec<UserDetail>>> {
72 let mut conn = pool.acquire().await?;
73
74 let token = authorize(
75 &mut conn,
76 Act::ViewUserProgressOrDetails,
77 Some(user.id),
78 Res::GlobalPermissions,
79 )
80 .await?;
81 let res =
82 models::user_details::search_for_user_details_by_other_details(&mut conn, &payload.query)
83 .await?;
84 token.authorized_ok(web::Json(res))
85}
86
87#[instrument(skip(pool))]
91pub async fn search_users_fuzzy_match(
92 user: AuthUser,
93 pool: web::Data<PgPool>,
94 payload: web::Json<SearchRequest>,
95) -> ControllerResult<web::Json<Vec<UserDetail>>> {
96 let mut conn = pool.acquire().await?;
97
98 let token = authorize(
99 &mut conn,
100 Act::ViewUserProgressOrDetails,
101 Some(user.id),
102 Res::GlobalPermissions,
103 )
104 .await?;
105 let res = models::user_details::search_for_user_details_fuzzy_match(&mut conn, &payload.query)
106 .await?;
107 token.authorized_ok(web::Json(res))
108}
109
110pub async fn get_users_by_course_id(
114 course_id: web::Path<Uuid>,
115 user: AuthUser,
116 pool: web::Data<PgPool>,
117) -> ControllerResult<web::Json<Vec<UserDetail>>> {
118 let mut conn = pool.acquire().await?;
119
120 let token = authorize(
121 &mut conn,
122 Act::ViewUserProgressOrDetails,
123 Some(user.id),
124 Res::Course(*course_id),
125 )
126 .await?;
127 let res = models::user_details::get_users_by_course_id(&mut conn, *course_id).await?;
128 token.authorized_ok(web::Json(res))
129}
130
131#[instrument(skip(pool))]
136pub async fn get_bulk_user_details(
137 user: AuthUser,
138 pool: web::Data<PgPool>,
139 payload: web::Json<BulkUserDetailsRequest>,
140) -> ControllerResult<web::Json<Vec<UserDetail>>> {
141 let mut conn = pool.acquire().await?;
142
143 let token = authorize(
144 &mut conn,
145 Act::ViewUserProgressOrDetails,
146 Some(user.id),
147 Res::Course(payload.course_id),
148 )
149 .await?;
150 let res = models::user_details::get_user_details_by_user_ids_for_course(
151 &mut conn,
152 &payload.user_ids,
153 payload.course_id,
154 )
155 .await?;
156 token.authorized_ok(web::Json(res))
157}
158
159#[instrument(skip(pool))]
163pub async fn get_user_details_for_user(
164 user: AuthUser,
165 pool: web::Data<PgPool>,
166) -> ControllerResult<web::Json<UserDetail>> {
167 let mut conn = pool.acquire().await?;
168
169 let token = skip_authorize();
170 let user_id = user.id;
171 let res = models::user_details::get_user_details_by_user_id(&mut conn, user_id).await?;
172 token.authorized_ok(web::Json(res))
173}
174
175pub async fn get_user_country_by_ip(
176 req: HttpRequest,
177 ip_to_country_mapper: web::Data<IpToCountryMapper>,
178) -> ControllerResult<String> {
179 let connection_info = req.connection_info();
180
181 let ip: Option<IpAddr> = connection_info
182 .realip_remote_addr()
183 .and_then(|ip| ip.parse::<IpAddr>().ok());
184
185 let country = ip
186 .and_then(|ip| ip_to_country_mapper.map_ip_to_country(&ip))
187 .map(|c| c.to_string())
188 .unwrap_or_default();
189
190 let token = skip_authorize();
191 token.authorized_ok(country)
192}
193
194#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
195#[cfg_attr(feature = "ts_rs", derive(TS))]
196pub struct UserInfoPayload {
197 pub email: String,
198 pub first_name: String,
199 pub last_name: String,
200 pub country: String,
201 pub email_communication_consent: bool,
202}
203
204#[instrument(skip(pool, app_conf, tmc_client))]
208pub async fn update_user_info(
209 user: AuthUser,
210 pool: web::Data<PgPool>,
211 payload: web::Json<UserInfoPayload>,
212 tmc_client: web::Data<TmcClient>,
213 app_conf: web::Data<ApplicationConfiguration>,
214) -> ControllerResult<web::Json<UserDetail>> {
215 let mut tx = pool.begin().await?;
216
217 let existing_user = models::user_details::get_user_details_by_user_id(&mut tx, user.id)
218 .await
219 .context("Failed to fetch existing user data")?;
220
221 let user = models::users::get_by_id(&mut tx, user.id)
222 .await
223 .context("Failed to fetch user")?;
224
225 let updated_user = models::user_details::update_user_info(
226 &mut tx,
227 user.id,
228 &payload.email,
229 &payload.first_name,
230 &payload.last_name,
231 &payload.country,
232 payload.email_communication_consent,
233 )
234 .await
235 .context("Failed to update database")?;
236
237 let email_changed = existing_user.email != payload.email;
238 let first_name_changed = existing_user.first_name != Some(payload.first_name.clone());
239 let last_name_changed = existing_user.last_name != Some(payload.last_name.clone());
240
241 if !app_conf.test_mode && (email_changed || first_name_changed || last_name_changed) {
242 let email_opt = if email_changed {
243 Some(payload.email.clone())
244 } else {
245 None
246 };
247
248 let upstream_id = user
249 .upstream_id
250 .ok_or_else(|| {
251 ControllerError::new(
252 ControllerErrorType::InternalServerError,
253 "Missing upstream_id".to_string(),
254 None,
255 )
256 })?
257 .to_string();
258
259 controllers::auth::update_user_information_to_tmc(
260 payload.first_name.clone(),
261 payload.last_name.clone(),
262 email_opt,
263 upstream_id,
264 tmc_client.clone(),
265 app_conf,
266 )
267 .await
268 .map_err(|e| {
269 ControllerError::new(
270 ControllerErrorType::InternalServerError,
271 "Failed to update user info to tmc",
272 e,
273 )
274 })?;
275 } else {
276 info!("User info unchanged, skipping update to TMC.");
277 }
278
279 tx.commit().await?;
280 let token = skip_authorize();
281 token.authorized_ok(web::Json(updated_user))
282}
283
284pub fn _add_routes(cfg: &mut ServiceConfig) {
285 cfg.route("/search-by-email", web::post().to(search_users_by_email))
286 .route(
287 "/search-by-other-details",
288 web::post().to(search_users_by_other_details),
289 )
290 .route(
291 "/search-fuzzy-match",
292 web::post().to(search_users_fuzzy_match),
293 )
294 .route(
295 "/{course_id}/user/{user_id}",
296 web::get().to(get_user_details),
297 )
298 .route("/users-ip-country", web::get().to(get_user_country_by_ip))
299 .route(
300 "/user-details-for-user",
301 web::get().to(get_user_details_for_user),
302 )
303 .route("/update-user-info", web::post().to(update_user_info))
304 .route(
305 "/{course_id}/get-users-by-course-id",
306 web::get().to(get_users_by_course_id),
307 )
308 .route("/bulk-user-details", web::post().to(get_bulk_user_details));
309}