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#[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#[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#[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#[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
99pub 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#[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#[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}