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::CourseEnrollmentsInfo, 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-enrollments`
28*/
29#[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/**
57POST `/api/v0/main-frontend/users/user-research-consents` - Adds a research consent for a student.
58*/
59#[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/**
79GET `/api/v0/main-frontend/users/get-user-research-consent` - Gets users research consent.
80*/
81#[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/**
96GET `/api/v0/main-frontend/users/get-user-research-consents` - Gets all users research consents for a course specific research form.
97*/
98#[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/**
114GET `/api/v0/main-frontend/users/my-courses` - Gets all the courses the user has either started or gotten a permission to.
115*/
116#[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/**
144GET `/api/v0/main-frontend/users/:id/user-reset-exercise-logs` - Get all logs of reset exercises for a user
145*/
146#[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 the user does not exist in the courses.mooc.fi database,
202            // check TMC for the user and create a new user in courses.mooc.fi if found.
203            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}