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