headless_lms_server/controllers/main_frontend/
users.rs

1use 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/**
12GET `/api/v0/main-frontend/users/:id`
13*/
14#[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/**
27GET `/api/v0/main-frontend/users/:id/course-instance-enrollments`
28*/
29#[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/**
62POST `/api/v0/main-frontend/users/user-research-consents` - Adds a research consent for a student.
63*/
64#[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/**
84GET `/api/v0/main-frontend/users/get-user-research-consent` - Gets users research consent.
85*/
86#[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/**
101GET `/api/v0/main-frontend/users/get-user-research-consents` - Gets all users research consents for a course specific research form.
102*/
103#[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/**
119GET `/api/v0/main-frontend/users/my-courses` - Gets all the courses the user has either started or gotten a permission to.
120*/
121#[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/**
149GET `/api/v0/main-frontend/users/:id/user-reset-exercise-logs` - Get all logs of reset exercises for a user
150*/
151#[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 the user does not exist in the courses.mooc.fi database,
207            // check TMC for the user and create a new user in courses.mooc.fi if found.
208            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}