headless_lms_server/controllers/tmc_server/
users.rs

1/*!
2Handlers for HTTP requests to `/api/v0/tmc-server/users/`.
3
4These endpoints are used by the TMC server to verify whether a user's email and password match
5what is stored in this system.
6
7This endpoint is intended to be used exclusively by the TMC server, and access requires
8a valid authorization header.
9*/
10
11use crate::domain::authorization::authorize_access_from_tmc_server_to_course_mooc_fi;
12use crate::prelude::*;
13use secrecy::SecretString;
14
15#[derive(Debug, Deserialize)]
16pub struct LoginRequest {
17    user_id: Uuid,
18    password: SecretString,
19}
20
21/**
22POST `/api/v0/tmc-server/users/authenticate`
23
24Endpoint used by the TMC server to authenticate a user using user_id and password.
25
26Returns `true` if the credentials match a known user in this system, otherwise returns `false`.
27
28Only works if the authorization header is set to a valid shared secret between systems.
29*/
30#[instrument(skip(pool))]
31pub async fn courses_moocfi_password_login(
32    request: HttpRequest,
33    pool: web::Data<PgPool>,
34    payload: web::Json<LoginRequest>,
35) -> ControllerResult<web::Json<bool>> {
36    let token = authorize_access_from_tmc_server_to_course_mooc_fi(&request).await?;
37
38    let mut conn = pool.acquire().await?;
39
40    let LoginRequest { user_id, password } = payload.into_inner();
41
42    let is_valid = models::user_passwords::verify_user_password(&mut conn, user_id, &password)
43        .await
44        .unwrap_or(false);
45
46    token.authorized_ok(web::Json(is_valid))
47}
48
49#[derive(Debug, Deserialize)]
50pub struct PasswordChangeRequest {
51    user_id: Uuid,
52    old_password: Option<SecretString>,
53    new_password: SecretString,
54}
55
56/**
57POST `/api/v0/tmc-server/users/change-password`
58
59Endpoint called by the TMC server when a user's password is changed.
60
61Only works if the authorization header is set to a valid shared secret between systems.
62*/
63#[instrument(skip(pool))]
64pub async fn courses_moocfi_password_change(
65    request: HttpRequest,
66    pool: web::Data<PgPool>,
67    payload: web::Json<PasswordChangeRequest>,
68) -> ControllerResult<web::Json<bool>> {
69    let token = authorize_access_from_tmc_server_to_course_mooc_fi(&request).await?;
70
71    let mut conn = pool.acquire().await?;
72
73    let PasswordChangeRequest {
74        user_id,
75        old_password,
76        new_password,
77    } = payload.into_inner();
78
79    // Verify old password if it is not None
80    if let Some(old) = old_password {
81        let is_user_valid = models::user_passwords::verify_user_password(&mut conn, user_id, &old)
82            .await
83            .unwrap_or(false);
84        if !is_user_valid {
85            return token.authorized_ok(web::Json(false));
86        }
87    }
88
89    let new_password_hash = match models::user_passwords::hash_password(&new_password) {
90        Ok(hash) => hash,
91        Err(_) => return token.authorized_ok(web::Json(false)),
92    };
93
94    let update_ok =
95        models::user_passwords::upsert_user_password(&mut conn, user_id, &new_password_hash)
96            .await
97            .unwrap_or(false);
98
99    token.authorized_ok(web::Json(update_ok))
100}
101
102pub fn _add_routes(cfg: &mut ServiceConfig) {
103    cfg.route(
104        "/authenticate",
105        web::post().to(courses_moocfi_password_login),
106    )
107    .route(
108        "/change-password",
109        web::post().to(courses_moocfi_password_change),
110    );
111}