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