Skip to main content

headless_lms_server/controllers/tmc_server/
users_by_upstream_id.rs

1/*!
2Handlers for HTTP requests to `/api/v0/tmc-server/users-by-upstream-id`.
3
4These endpoints are used by the TMC server so that it can integrate with this system.
5*/
6
7use crate::{
8    domain::authentication::{
9        authenticate_tmc_server, get_or_create_user_from_tmc_mooc_fi_response,
10    },
11    prelude::*,
12};
13use headless_lms_utils::services::tmc::TmcClient;
14use models::users::User;
15
16/**
17GET `/api/v0/tmc-server/users-by-upstream-id/:id` Endpoint that TMC server uses to get user information by using its own ids.
18
19Only works if the authorization header is set to a secret value.
20*/
21#[instrument(skip(pool))]
22pub async fn get_user_by_upstream_id(
23    upstream_id: web::Path<i32>,
24    pool: web::Data<PgPool>,
25    request: HttpRequest,
26    tmc_client: web::Data<TmcClient>,
27) -> ControllerResult<web::Json<User>> {
28    let mut conn = pool.acquire().await?;
29    let token = authenticate_tmc_server(&request).await?;
30    let tmc_user = tmc_client
31        .get_user_from_tmc_mooc_fi_by_tmc_access_token_and_upstream_id(&upstream_id)
32        .await?;
33
34    debug!(
35        "Creating or fetching user with TMC id {} and mooc.fi UUID {}",
36        tmc_user.id,
37        tmc_user
38            .courses_mooc_fi_user_id
39            .map(|uuid| uuid.to_string())
40            .unwrap_or_else(|| "None (will generate new UUID)".to_string())
41    );
42    let user = get_or_create_user_from_tmc_mooc_fi_response(
43        &mut conn,
44        tmc_user,
45        tmc_client.get_admin_access_token(),
46    )
47    .await?;
48    info!(
49        "Successfully got user details from mooc.fi for user {}",
50        user.id
51    );
52
53    token.authorized_ok(web::Json(user))
54}
55
56#[derive(Debug, Serialize)]
57pub struct UserMigrationStatusResponse {
58    pub shadow_user_exists: bool,
59    pub courses_mooc_fi_user_id: Option<Uuid>,
60    pub password_set: bool,
61    pub deleted_at: Option<DateTime<Utc>>,
62}
63
64/**
65GET `/api/v0/tmc-server/users-by-upstream-id/:id/status` Read-only status check for the TMC
66server's admin UI: does a courses.mooc.fi shadow user exist for this upstream id, and does it
67already have a password set. Unlike `get_user_by_upstream_id`, this never calls tmc.mooc.fi and
68never creates a user -- it's a plain read, safe to call on every admin page view.
69
70Only works if the authorization header is set to a secret value.
71*/
72#[instrument(skip(pool))]
73pub async fn get_migration_status_by_upstream_id(
74    upstream_id: web::Path<i32>,
75    pool: web::Data<PgPool>,
76    request: HttpRequest,
77) -> ControllerResult<web::Json<UserMigrationStatusResponse>> {
78    let token = authenticate_tmc_server(&request).await?;
79    let mut conn = pool.acquire().await?;
80
81    let response = match models::users::find_by_upstream_id(&mut conn, *upstream_id).await? {
82        Some(user) => {
83            let password_set =
84                models::user_passwords::check_if_users_password_is_stored(&mut conn, user.id)
85                    .await?;
86            UserMigrationStatusResponse {
87                shadow_user_exists: true,
88                courses_mooc_fi_user_id: Some(user.id),
89                password_set,
90                deleted_at: user.deleted_at,
91            }
92        }
93        None => UserMigrationStatusResponse {
94            shadow_user_exists: false,
95            courses_mooc_fi_user_id: None,
96            password_set: false,
97            deleted_at: None,
98        },
99    };
100
101    token.authorized_ok(web::Json(response))
102}
103
104pub fn _add_routes(cfg: &mut ServiceConfig) {
105    cfg.route("/{user_id}", web::get().to(get_user_by_upstream_id))
106        .route(
107            "/{user_id}/status",
108            web::get().to(get_migration_status_by_upstream_id),
109        );
110}
111
112#[cfg(test)]
113mod test {
114    use super::*;
115    use crate::test_helper::*;
116    use models::user_passwords;
117    use models::users;
118    use secrecy::SecretString;
119
120    #[actix_web::test]
121    async fn migration_status_reflects_shadow_user_and_password_state() {
122        let mut conn = Conn::init().await;
123        let mut tx = conn.begin().await;
124
125        let upstream_id = 987_654_321;
126        let moocfi_id = Uuid::new_v4();
127
128        assert!(
129            users::find_by_upstream_id(tx.as_mut(), upstream_id)
130                .await
131                .unwrap()
132                .is_none()
133        );
134
135        let user = users::insert_with_upstream_id_and_moocfi_id(
136            tx.as_mut(),
137            "migration-status-test@example.com",
138            None,
139            None,
140            upstream_id,
141            moocfi_id,
142        )
143        .await
144        .unwrap();
145
146        let found = users::find_by_upstream_id(tx.as_mut(), upstream_id)
147            .await
148            .unwrap()
149            .unwrap();
150        assert_eq!(found.id, user.id);
151        assert!(
152            !user_passwords::check_if_users_password_is_stored(tx.as_mut(), user.id)
153                .await
154                .unwrap()
155        );
156
157        let hash =
158            user_passwords::hash_password(&SecretString::new("test-password".to_string().into()))
159                .unwrap();
160        user_passwords::upsert_user_password(tx.as_mut(), user.id, &hash)
161            .await
162            .unwrap();
163
164        assert!(
165            user_passwords::check_if_users_password_is_stored(tx.as_mut(), user.id)
166                .await
167                .unwrap()
168        );
169    }
170}