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#[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/**
22GET `/api/v0/main-frontend/user-details/{course_id}/user/{user_id}` - Find user details by user id with course permission check
23Only returns user details if the user is enrolled in the specified course
24*/
25#[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/**
48POST `/api/v0/main-frontend/user-details/user-by-courses` - Find user details by user id with multi-course permission check
49Returns user details if the user has permission to view user details through any of the specified courses
50*/
51#[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    // Check if the user has permission to view user details through any of the provided courses
60    let mut token = None;
61
62    if payload.course_ids.is_empty() {
63        // One can view the details though global permissions even though they have not started any course yet
64        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    // If we have permission, get the user details without course restriction
98    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/**
103GET `/api/v0/main-frontend/user-details/search-by-email` - Allows to search user by their email
104*/
105#[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/**
126GET `/api/v0/main-frontend/user-details/search-by-other-details` - Allows to search user by their names etc.
127*/
128#[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/**
150GET `/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
151*/
152#[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
172/**
173GET `/api/v0/main-frontend/user-details/get-users-by-course-id` - Get user details of users that are in the course
174*/
175pub 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/**
194POST `/api/v0/main-frontend/user-details/bulk-user-details` - Get user details for a list of user IDs with course permission check
195Only returns user details for users who are actually enrolled in the specified course
196*/
197#[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/**
222GET `/api/v0/main-frontend/user-details/user-details-for-user` - Get authenticated user's own details
223*/
224#[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/**
267POST `/api/v0/main-frontend/user-details/update-user-info` - Updates the users information such as email, name, country and email communication consent
268*/
269#[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}