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/**
8GET `/api/v0/main-frontend/user-details/[id]` - Find user details by user id
9*/
10#[instrument(skip(pool))]
11pub async fn get_user_details(
12    user: AuthUser,
13    pool: web::Data<PgPool>,
14    user_id: web::Path<Uuid>,
15) -> ControllerResult<web::Json<UserDetail>> {
16    let mut conn = pool.acquire().await?;
17
18    let token = authorize(
19        &mut conn,
20        Act::ViewUserProgressOrDetails,
21        Some(user.id),
22        Res::GlobalPermissions,
23    )
24    .await?;
25    let res = models::user_details::get_user_details_by_user_id(&mut conn, *user_id).await?;
26    token.authorized_ok(web::Json(res))
27}
28
29/**
30GET `/api/v0/main-frontend/user-details/search-by-email` - Allows to search user by their email
31*/
32#[instrument(skip(pool))]
33pub async fn search_users_by_email(
34    user: AuthUser,
35    pool: web::Data<PgPool>,
36    payload: web::Json<SearchRequest>,
37) -> ControllerResult<web::Json<Vec<UserDetail>>> {
38    let mut conn = pool.acquire().await?;
39
40    let token = authorize(
41        &mut conn,
42        Act::ViewUserProgressOrDetails,
43        Some(user.id),
44        Res::GlobalPermissions,
45    )
46    .await?;
47    let res =
48        models::user_details::search_for_user_details_by_email(&mut conn, &payload.query).await?;
49    token.authorized_ok(web::Json(res))
50}
51
52/**
53GET `/api/v0/main-frontend/user-details/search-by-other-details` - Allows to search user by their names etc.
54*/
55#[instrument(skip(pool))]
56pub async fn search_users_by_other_details(
57    user: AuthUser,
58    pool: web::Data<PgPool>,
59    payload: web::Json<SearchRequest>,
60) -> ControllerResult<web::Json<Vec<UserDetail>>> {
61    let mut conn = pool.acquire().await?;
62
63    let token = authorize(
64        &mut conn,
65        Act::ViewUserProgressOrDetails,
66        Some(user.id),
67        Res::GlobalPermissions,
68    )
69    .await?;
70    let res =
71        models::user_details::search_for_user_details_by_other_details(&mut conn, &payload.query)
72            .await?;
73    token.authorized_ok(web::Json(res))
74}
75
76/**
77GET `/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
78*/
79#[instrument(skip(pool))]
80pub async fn search_users_fuzzy_match(
81    user: AuthUser,
82    pool: web::Data<PgPool>,
83    payload: web::Json<SearchRequest>,
84) -> ControllerResult<web::Json<Vec<UserDetail>>> {
85    let mut conn = pool.acquire().await?;
86
87    let token = authorize(
88        &mut conn,
89        Act::ViewUserProgressOrDetails,
90        Some(user.id),
91        Res::GlobalPermissions,
92    )
93    .await?;
94    let res = models::user_details::search_for_user_details_fuzzy_match(&mut conn, &payload.query)
95        .await?;
96    token.authorized_ok(web::Json(res))
97}
98
99/**
100GET `/api/v0/main-frontend/user-details/get-users-by-course-id` - Get user details of users that are in the course
101*/
102pub async fn get_users_by_course_id(
103    course_id: web::Path<Uuid>,
104    user: AuthUser,
105    pool: web::Data<PgPool>,
106) -> ControllerResult<web::Json<Vec<UserDetail>>> {
107    let mut conn = pool.acquire().await?;
108
109    let token = authorize(
110        &mut conn,
111        Act::ViewUserProgressOrDetails,
112        Some(user.id),
113        Res::Course(*course_id),
114    )
115    .await?;
116    let res = models::user_details::get_users_by_course_id(&mut conn, *course_id).await?;
117    token.authorized_ok(web::Json(res))
118}
119
120/**
121GET `/api/v0/main-frontend/user-details/user-details-for-user` - Get authenticated user's own details
122*/
123#[instrument(skip(pool))]
124pub async fn get_user_details_for_user(
125    user: AuthUser,
126    pool: web::Data<PgPool>,
127) -> ControllerResult<web::Json<UserDetail>> {
128    let mut conn = pool.acquire().await?;
129
130    let token = skip_authorize();
131    let user_id = user.id;
132    let res = models::user_details::get_user_details_by_user_id(&mut conn, user_id).await?;
133    token.authorized_ok(web::Json(res))
134}
135
136pub async fn get_user_country_by_ip(
137    req: HttpRequest,
138    ip_to_country_mapper: web::Data<IpToCountryMapper>,
139) -> ControllerResult<String> {
140    let connection_info = req.connection_info();
141
142    let ip: Option<IpAddr> = connection_info
143        .realip_remote_addr()
144        .and_then(|ip| ip.parse::<IpAddr>().ok());
145
146    let country = ip
147        .and_then(|ip| ip_to_country_mapper.map_ip_to_country(&ip))
148        .map(|c| c.to_string())
149        .unwrap_or_default();
150
151    let token = skip_authorize();
152    token.authorized_ok(country)
153}
154
155#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
156#[cfg_attr(feature = "ts_rs", derive(TS))]
157pub struct UserInfoPayload {
158    pub email: String,
159    pub first_name: String,
160    pub last_name: String,
161    pub country: String,
162    pub email_communication_consent: bool,
163}
164
165/**
166POST `/api/v0/main-frontend/user-details/update-user-info` - Updates the users information such as email, name, country and email communication consent
167*/
168#[instrument(skip(pool, app_conf, tmc_client))]
169pub async fn update_user_info(
170    user: AuthUser,
171    pool: web::Data<PgPool>,
172    payload: web::Json<UserInfoPayload>,
173    tmc_client: web::Data<TmcClient>,
174    app_conf: web::Data<ApplicationConfiguration>,
175) -> ControllerResult<web::Json<UserDetail>> {
176    let mut tx = pool.begin().await?;
177
178    let existing_user = models::user_details::get_user_details_by_user_id(&mut tx, user.id)
179        .await
180        .context("Failed to fetch existing user data")?;
181
182    let user = models::users::get_by_id(&mut tx, user.id)
183        .await
184        .context("Failed to fetch user")?;
185
186    let updated_user = models::user_details::update_user_info(
187        &mut tx,
188        user.id,
189        &payload.email,
190        &payload.first_name,
191        &payload.last_name,
192        &payload.country,
193        payload.email_communication_consent,
194    )
195    .await
196    .context("Failed to update database")?;
197
198    let email_changed = existing_user.email != payload.email;
199    let first_name_changed = existing_user.first_name != Some(payload.first_name.clone());
200    let last_name_changed = existing_user.last_name != Some(payload.last_name.clone());
201
202    if !app_conf.test_mode && (email_changed || first_name_changed || last_name_changed) {
203        let email_opt = if email_changed {
204            Some(payload.email.clone())
205        } else {
206            None
207        };
208
209        let upstream_id = user
210            .upstream_id
211            .ok_or_else(|| {
212                ControllerError::new(
213                    ControllerErrorType::InternalServerError,
214                    "Missing upstream_id".to_string(),
215                    None,
216                )
217            })?
218            .to_string();
219
220        controllers::auth::update_user_information_to_tmc(
221            payload.first_name.clone(),
222            payload.last_name.clone(),
223            email_opt,
224            upstream_id,
225            tmc_client.clone(),
226            app_conf,
227        )
228        .await
229        .map_err(|e| {
230            ControllerError::new(
231                ControllerErrorType::InternalServerError,
232                "Failed to update user info to tmc",
233                e,
234            )
235        })?;
236    } else {
237        info!("User info unchanged, skipping update to TMC.");
238    }
239
240    tx.commit().await?;
241    let token = skip_authorize();
242    token.authorized_ok(web::Json(updated_user))
243}
244
245pub fn _add_routes(cfg: &mut ServiceConfig) {
246    cfg.route("/search-by-email", web::post().to(search_users_by_email))
247        .route(
248            "/search-by-other-details",
249            web::post().to(search_users_by_other_details),
250        )
251        .route(
252            "/search-fuzzy-match",
253            web::post().to(search_users_fuzzy_match),
254        )
255        .route("/user/{user_id}", web::get().to(get_user_details))
256        .route("/users-ip-country", web::get().to(get_user_country_by_ip))
257        .route(
258            "/user-details-for-user",
259            web::get().to(get_user_details_for_user),
260        )
261        .route("/update-user-info", web::post().to(update_user_info))
262        .route(
263            "/{course_id}/get-users-by-course-id",
264            web::get().to(get_users_by_course_id),
265        );
266}