headless_lms_server/controllers/main_frontend/
user_details.rs

1use 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/**
15GET `/api/v0/main-frontend/user-details/{course_id}/user/{user_id}` - Find user details by user id with course permission check
16Only returns user details if the user is enrolled in the specified course
17*/
18#[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/**
41GET `/api/v0/main-frontend/user-details/search-by-email` - Allows to search user by their email
42*/
43#[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/**
64GET `/api/v0/main-frontend/user-details/search-by-other-details` - Allows to search user by their names etc.
65*/
66#[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/**
88GET `/api/v0/main-frontend/user-details/search-fuzzy-match` - Allows to find the right user details in cases where there is a small typing error in the search query
89*/
90#[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
110/**
111GET `/api/v0/main-frontend/user-details/get-users-by-course-id` - Get user details of users that are in the course
112*/
113pub 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/**
132POST `/api/v0/main-frontend/user-details/bulk-user-details` - Get user details for a list of user IDs with course permission check
133Only returns user details for users who are actually enrolled in the specified course
134*/
135#[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/**
160GET `/api/v0/main-frontend/user-details/user-details-for-user` - Get authenticated user's own details
161*/
162#[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/**
205POST `/api/v0/main-frontend/user-details/update-user-info` - Updates the users information such as email, name, country and email communication consent
206*/
207#[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}