headless_lms_server/controllers/tmc_server/
users.rs1use crate::domain::authorization::{
16 authorize_access_from_tmc_server_to_course_mooc_fi,
17 get_or_create_user_from_tmc_mooc_fi_response,
18};
19use crate::prelude::*;
20use headless_lms_utils::tmc::TmcClient;
21use models::users::User;
22use secrecy::SecretString;
23
24#[derive(Debug, Deserialize)]
25pub struct CreateUserRequest {
26 upstream_id: i32,
27 password: SecretString,
28}
29
30#[derive(Debug, Serialize)]
31pub struct CreateUserResponse {
32 pub user: User,
33 pub password_set: bool,
34}
35
36#[derive(Debug, Deserialize)]
37pub struct LoginRequest {
38 user_id: Uuid,
39 password: SecretString,
40}
41
42#[instrument(skip(pool, tmc_client))]
55pub async fn create_user(
56 request: HttpRequest,
57 pool: web::Data<PgPool>,
58 payload: web::Json<CreateUserRequest>,
59 tmc_client: web::Data<TmcClient>,
60) -> ControllerResult<web::Json<CreateUserResponse>> {
61 let token = authorize_access_from_tmc_server_to_course_mooc_fi(&request).await?;
62
63 let CreateUserRequest {
64 upstream_id,
65 password,
66 } = payload.into_inner();
67
68 let tmc_user = tmc_client
69 .get_user_from_tmc_mooc_fi_by_tmc_access_token_and_upstream_id(&upstream_id)
70 .await?;
71
72 info!(
73 "Creating or fetching user with TMC id {} and mooc.fi UUID {}",
74 tmc_user.id,
75 tmc_user
76 .courses_mooc_fi_user_id
77 .map(|uuid| uuid.to_string())
78 .unwrap_or_else(|| "None (will generate new UUID)".to_string())
79 );
80
81 let mut tx = pool.begin().await?;
83
84 let user = get_or_create_user_from_tmc_mooc_fi_response(
85 &mut tx,
86 tmc_user,
87 tmc_client.get_admin_access_token(),
88 )
89 .await?;
90
91 info!("User {} created or fetched successfully", user.id);
92
93 let password_hash = models::user_passwords::hash_password(&password).map_err(|e| {
94 ControllerError::new(
95 ControllerErrorType::InternalServerError,
96 "Failed to hash password",
97 Some(anyhow::Error::msg(e.to_string())),
98 )
99 })?;
100 let password_set =
101 models::user_passwords::upsert_user_password(&mut tx, user.id, &password_hash).await?;
102
103 tx.commit().await?;
104
105 super::notify_password_managed_with_retry(&tmc_client, upstream_id.to_string(), user.id).await;
108
109 info!("Password set: {}", password_set);
110
111 token.authorized_ok(web::Json(CreateUserResponse { user, password_set }))
112}
113
114#[instrument(skip(pool))]
124pub async fn courses_moocfi_password_login(
125 request: HttpRequest,
126 pool: web::Data<PgPool>,
127 payload: web::Json<LoginRequest>,
128) -> ControllerResult<web::Json<bool>> {
129 let token = authorize_access_from_tmc_server_to_course_mooc_fi(&request).await?;
130
131 let mut conn = pool.acquire().await?;
132
133 let LoginRequest { user_id, password } = payload.into_inner();
134
135 let is_valid = models::user_passwords::verify_user_password(&mut conn, user_id, &password)
136 .await
137 .unwrap_or_else(|e| {
138 warn!("Password verification errored for user {user_id}: {e}");
140 false
141 });
142
143 token.authorized_ok(web::Json(is_valid))
144}
145
146#[derive(Debug, Deserialize)]
147pub struct PasswordChangeRequest {
148 user_id: Uuid,
149 old_password: Option<SecretString>,
150 new_password: SecretString,
151}
152
153#[instrument(skip(pool))]
161pub async fn courses_moocfi_password_change(
162 request: HttpRequest,
163 pool: web::Data<PgPool>,
164 payload: web::Json<PasswordChangeRequest>,
165) -> ControllerResult<web::Json<bool>> {
166 let token = authorize_access_from_tmc_server_to_course_mooc_fi(&request).await?;
167
168 let mut conn = pool.acquire().await?;
169
170 let PasswordChangeRequest {
171 user_id,
172 old_password,
173 new_password,
174 } = payload.into_inner();
175
176 if let Some(old) = old_password {
178 let is_user_valid = models::user_passwords::verify_user_password(&mut conn, user_id, &old)
179 .await
180 .unwrap_or_else(|e| {
181 warn!("Old-password verification errored for user {user_id}: {e}");
182 false
183 });
184 if !is_user_valid {
185 return token.authorized_ok(web::Json(false));
186 }
187 }
188
189 let new_password_hash = match models::user_passwords::hash_password(&new_password) {
190 Ok(hash) => hash,
191 Err(e) => {
192 warn!("Failed to hash new password for user {user_id}: {e}");
193 return token.authorized_ok(web::Json(false));
194 }
195 };
196
197 let update_ok =
198 models::user_passwords::upsert_user_password(&mut conn, user_id, &new_password_hash)
199 .await
200 .unwrap_or_else(|e| {
201 warn!("Failed to store new password for user {user_id}: {e}");
202 false
203 });
204
205 token.authorized_ok(web::Json(update_ok))
206}
207
208pub fn _add_routes(cfg: &mut ServiceConfig) {
209 cfg.route("/create", web::post().to(create_user))
210 .route(
211 "/authenticate",
212 web::post().to(courses_moocfi_password_login),
213 )
214 .route(
215 "/change-password",
216 web::post().to(courses_moocfi_password_change),
217 );
218}