headless_lms_server/controllers/main_frontend/
users.rs1use crate::prelude::*;
2use anyhow::anyhow;
3use headless_lms_utils::tmc::TmcClient;
4use models::{
5 course_instance_enrollments::CourseInstanceEnrollmentsInfo, courses::Course,
6 exercise_reset_logs::ExerciseResetLog, research_forms::ResearchFormQuestionAnswer,
7 user_research_consents::UserResearchConsent, users::User,
8};
9use secrecy::SecretString;
10
11#[instrument(skip(pool))]
15pub async fn get_user(
16 user_id: web::Path<Uuid>,
17 pool: web::Data<PgPool>,
18) -> ControllerResult<web::Json<User>> {
19 let mut conn = pool.acquire().await?;
20 let user = models::users::get_by_id(&mut conn, *user_id).await?;
21
22 let token = authorize(&mut conn, Act::Teach, Some(*user_id), Res::AnyCourse).await?;
23 token.authorized_ok(web::Json(user))
24}
25
26#[instrument(skip(pool))]
30pub async fn get_course_instance_enrollments_for_user(
31 user_id: web::Path<Uuid>,
32 pool: web::Data<PgPool>,
33 auth_user: AuthUser,
34) -> ControllerResult<web::Json<CourseInstanceEnrollmentsInfo>> {
35 let mut conn = pool.acquire().await?;
36 let token = authorize(
37 &mut conn,
38 Act::ViewUserProgressOrDetails,
39 Some(auth_user.id),
40 Res::GlobalPermissions,
41 )
42 .await?;
43 let mut res =
44 models::course_instance_enrollments::get_course_instance_enrollments_info_for_user(
45 &mut conn, *user_id,
46 )
47 .await?;
48
49 res.course_instance_enrollments
50 .sort_by(|a, b| a.created_at.cmp(&b.created_at));
51
52 token.authorized_ok(web::Json(res))
53}
54
55#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
56#[cfg_attr(feature = "ts_rs", derive(TS))]
57pub struct ConsentData {
58 pub consent: bool,
59}
60
61#[instrument(skip(pool))]
65pub async fn post_user_consents(
66 payload: web::Json<ConsentData>,
67 user: AuthUser,
68 pool: web::Data<PgPool>,
69) -> ControllerResult<web::Json<UserResearchConsent>> {
70 let mut conn = pool.acquire().await?;
71 let token = skip_authorize();
72
73 let res = models::user_research_consents::upsert(
74 &mut conn,
75 PKeyPolicy::Generate,
76 user.id,
77 payload.consent,
78 )
79 .await?;
80 token.authorized_ok(web::Json(res))
81}
82
83#[instrument(skip(pool))]
87pub async fn get_research_consent_by_user_id(
88 user: AuthUser,
89 pool: web::Data<PgPool>,
90) -> ControllerResult<web::Json<UserResearchConsent>> {
91 let mut conn = pool.acquire().await?;
92 let token = skip_authorize();
93
94 let res =
95 models::user_research_consents::get_research_consent_by_user_id(&mut conn, user.id).await?;
96
97 token.authorized_ok(web::Json(res))
98}
99
100#[instrument(skip(pool))]
104async fn get_all_research_form_answers_with_user_id(
105 user: AuthUser,
106 pool: web::Data<PgPool>,
107) -> ControllerResult<web::Json<Vec<ResearchFormQuestionAnswer>>> {
108 let mut conn = pool.acquire().await?;
109 let token = skip_authorize();
110
111 let res =
112 models::research_forms::get_all_research_form_answers_with_user_id(&mut conn, user.id)
113 .await?;
114
115 token.authorized_ok(web::Json(res))
116}
117
118#[instrument(skip(pool))]
122async fn get_my_courses(
123 user: AuthUser,
124 pool: web::Data<PgPool>,
125) -> ControllerResult<web::Json<Vec<Course>>> {
126 let mut conn = pool.acquire().await?;
127 let token = skip_authorize();
128
129 let courses_enrolled_to =
130 models::courses::all_courses_user_enrolled_to(&mut conn, user.id).await?;
131
132 let courses_with_roles =
133 models::courses::all_courses_with_roles_for_user(&mut conn, user.id).await?;
134
135 let combined = courses_enrolled_to
136 .clone()
137 .into_iter()
138 .chain(
139 courses_with_roles
140 .into_iter()
141 .filter(|c| !courses_enrolled_to.iter().any(|c2| c.id == c2.id)),
142 )
143 .collect();
144
145 token.authorized_ok(web::Json(combined))
146}
147
148#[instrument(skip(pool))]
152pub async fn get_user_reset_exercise_logs(
153 user_id: web::Path<Uuid>,
154 pool: web::Data<PgPool>,
155 auth_user: AuthUser,
156) -> ControllerResult<web::Json<Vec<ExerciseResetLog>>> {
157 let mut conn = pool.acquire().await?;
158 let token = authorize(
159 &mut conn,
160 Act::ViewUserProgressOrDetails,
161 Some(auth_user.id),
162 Res::GlobalPermissions,
163 )
164 .await?;
165 let res =
166 models::exercise_reset_logs::get_exercise_reset_logs_for_user(&mut conn, *user_id).await?;
167
168 token.authorized_ok(web::Json(res))
169}
170
171#[derive(Debug, Serialize, Deserialize)]
172#[cfg_attr(feature = "ts_rs", derive(TS))]
173pub struct EmailData {
174 pub email: String,
175 pub language: String,
176}
177
178#[instrument(skip(pool))]
179pub async fn send_reset_password_email(
180 pool: web::Data<PgPool>,
181 payload: web::Json<EmailData>,
182 tmc_client: web::Data<TmcClient>,
183) -> ControllerResult<web::Json<bool>> {
184 let mut conn = pool.acquire().await?;
185 let token = skip_authorize();
186
187 let email = &payload.email.trim().to_lowercase();
188 let language = &payload.language;
189
190 let reset_template = models::email_templates::get_generic_email_template_by_name_and_language(
191 &mut conn,
192 "reset-password-email",
193 language,
194 )
195 .await
196 .map_err(|_e| {
197 anyhow::anyhow!(
198 "Password reset email template not configured. Missing template 'reset-password-email' for language '{}'",
199 language
200 )
201 })?;
202
203 let user = match models::users::get_by_email(&mut conn, email).await {
204 Ok(user) => Some(user),
205 Err(_) => {
206 if let Ok(tmc_user) = tmc_client.get_user_from_tmc_with_email(email.clone()).await {
209 Some(
210 models::users::insert_with_upstream_id_and_moocfi_id(
211 &mut conn,
212 &tmc_user.email,
213 tmc_user.first_name.as_deref(),
214 tmc_user.last_name.as_deref(),
215 tmc_user.upstream_id,
216 tmc_user.id,
217 )
218 .await?,
219 )
220 } else {
221 None
222 }
223 }
224 };
225
226 if let Some(user) = user {
227 let token = Uuid::new_v4();
228
229 let _password_token =
230 models::user_passwords::insert_password_reset_token(&mut conn, user.id, token).await?;
231
232 let _ =
233 models::email_deliveries::insert_email_delivery(&mut conn, user.id, reset_template.id)
234 .await?;
235 }
236
237 token.authorized_ok(web::Json(true))
238}
239
240#[derive(Debug, Serialize, Deserialize)]
241pub struct ResetPasswordTokenPayload {
242 pub token: String,
243}
244
245#[instrument(skip(pool))]
246pub async fn reset_password_token_status(
247 pool: web::Data<PgPool>,
248 payload: web::Json<ResetPasswordTokenPayload>,
249) -> ControllerResult<web::Json<bool>> {
250 let mut conn = pool.acquire().await?;
251 let token = skip_authorize();
252
253 let password_token = match Uuid::parse_str(&payload.token) {
254 Ok(u) => u,
255 Err(_) => return token.authorized_ok(web::Json(false)),
256 };
257
258 let res =
259 models::user_passwords::is_reset_password_token_valid(&mut conn, &password_token).await?;
260
261 token.authorized_ok(web::Json(res))
262}
263
264#[derive(Debug, Serialize, Deserialize)]
265#[cfg_attr(feature = "ts_rs", derive(TS))]
266pub struct ResetPasswordData {
267 pub token: String,
268 pub new_password: String,
269}
270
271#[instrument(skip(pool))]
272pub async fn reset_user_password(
273 pool: web::Data<PgPool>,
274 payload: web::Json<ResetPasswordData>,
275 tmc_client: web::Data<TmcClient>,
276) -> ControllerResult<web::Json<bool>> {
277 let mut conn = pool.acquire().await?;
278 let token = skip_authorize();
279
280 let token_uuid = Uuid::parse_str(&payload.token)?;
281 let password_hash = models::user_passwords::hash_password(&SecretString::new(
282 payload.new_password.clone().into(),
283 ))
284 .map_err(|e| anyhow!("Failed to hash password: {:?}", e))?;
285
286 let res = models::user_passwords::change_user_password_with_password_reset_token(
287 &mut conn,
288 token_uuid,
289 &password_hash,
290 &tmc_client,
291 )
292 .await?;
293
294 token.authorized_ok(web::Json(res))
295}
296
297#[derive(Debug, Serialize, Deserialize)]
298#[cfg_attr(feature = "ts_rs", derive(TS))]
299pub struct ChangePasswordData {
300 pub old_password: String,
301 pub new_password: String,
302}
303
304#[instrument(skip(pool))]
305pub async fn change_user_password(
306 pool: web::Data<PgPool>,
307 payload: web::Json<ChangePasswordData>,
308 user: AuthUser,
309) -> ControllerResult<web::Json<bool>> {
310 let mut conn = pool.acquire().await?;
311 let token = skip_authorize();
312 let password_hash = models::user_passwords::hash_password(&SecretString::new(
313 payload.new_password.clone().into(),
314 ))
315 .map_err(|e| anyhow!("Failed to hash password: {:?}", e))?;
316 let old_password = SecretString::new(payload.old_password.clone().into());
317
318 let res = models::user_passwords::change_user_password_with_old_password(
319 &mut conn,
320 user.id,
321 &old_password,
322 &password_hash,
323 )
324 .await?;
325
326 token.authorized_ok(web::Json(res))
327}
328
329pub fn _add_routes(cfg: &mut ServiceConfig) {
330 cfg.route(
331 "/user-research-form-question-answers",
332 web::get().to(get_all_research_form_answers_with_user_id),
333 )
334 .route("/my-courses", web::get().to(get_my_courses))
335 .route(
336 "/get-user-research-consent",
337 web::get().to(get_research_consent_by_user_id),
338 )
339 .route(
340 "/user-research-consents",
341 web::post().to(post_user_consents),
342 )
343 .route(
344 "/send-reset-password-email",
345 web::post().to(send_reset_password_email),
346 )
347 .route("/{user_id}", web::get().to(get_user))
348 .route(
349 "/{user_id}/course-instance-enrollments",
350 web::get().to(get_course_instance_enrollments_for_user),
351 )
352 .route(
353 "/{user_id}/user-reset-exercise-logs",
354 web::get().to(get_user_reset_exercise_logs),
355 )
356 .route(
357 "/reset-password-token-status",
358 web::post().to(reset_password_token_status),
359 )
360 .route("/reset-password", web::post().to(reset_user_password))
361 .route("/change-password", web::post().to(change_user_password));
362}