1use crate::{
6 OAuthClient,
7 domain::{
8 authorization::{
9 self, ActionOnResource, authorize_with_fetched_list_of_roles, skip_authorize,
10 },
11 rate_limit_middleware_builder::build_rate_limiting_middleware,
12 },
13 prelude::*,
14};
15use actix_session::Session;
16use anyhow::Error;
17use anyhow::anyhow;
18use headless_lms_models::ModelResult;
19use headless_lms_utils::tmc::{NewUserInfo, TmcClient};
20use std::time::Duration;
21use tracing_log::log;
22
23#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
24#[cfg_attr(feature = "ts_rs", derive(TS))]
25pub struct Login {
26 email: String,
27 password: String,
28}
29
30#[instrument(skip(pool, payload,))]
35pub async fn authorize_action_on_resource(
36 pool: web::Data<PgPool>,
37 user: Option<AuthUser>,
38 payload: web::Json<ActionOnResource>,
39) -> ControllerResult<web::Json<bool>> {
40 let mut conn = pool.acquire().await?;
41 let data = payload.0;
42 if let Some(user) = user {
43 match authorize(&mut conn, data.action, Some(user.id), data.resource).await {
44 Ok(true_token) => true_token.authorized_ok(web::Json(true)),
45 _ => {
46 let false_token = skip_authorize();
48 false_token.authorized_ok(web::Json(false))
49 }
50 }
51 } else {
52 let false_token = skip_authorize();
54 false_token.authorized_ok(web::Json(false))
55 }
56}
57
58#[derive(Debug, Serialize, Deserialize)]
59#[cfg_attr(feature = "ts_rs", derive(TS))]
60pub struct CreateAccountDetails {
61 pub email: String,
62 pub first_name: String,
63 pub last_name: String,
64 pub language: String,
65 pub password: String,
66 pub password_confirmation: String,
67 pub country: String,
68 pub email_communication_consent: bool,
69}
70
71#[instrument(skip(session, pool, client, payload, app_conf))]
92pub async fn signup(
93 session: Session,
94 payload: web::Json<CreateAccountDetails>,
95 pool: web::Data<PgPool>,
96 client: web::Data<OAuthClient>,
97 user: Option<AuthUser>,
98 app_conf: web::Data<ApplicationConfiguration>,
99 tmc_client: web::Data<TmcClient>,
100) -> ControllerResult<HttpResponse> {
101 let user_details = payload.0;
102 let mut conn = pool.acquire().await?;
103
104 if app_conf.test_mode {
105 return handle_test_mode_signup(&mut conn, &session, &user_details, &app_conf).await;
106 }
107
108 if user.is_none() {
109 post_new_user_to_moocfi(&user_details, tmc_client, &app_conf).await?;
111
112 let auth_result = authorization::authenticate_moocfi_user(
113 &mut conn,
114 &client,
115 user_details.email,
116 user_details.password,
117 )
118 .await?;
119
120 if let Some((user, _token)) = auth_result {
121 let country = user_details.country.clone();
122 models::user_details::update_user_country(&mut conn, user.id, &country).await?;
123 models::user_details::update_user_email_communication_consent(
124 &mut conn,
125 user.id,
126 user_details.email_communication_consent,
127 )
128 .await?;
129
130 let token = skip_authorize();
131 authorization::remember(&session, user)?;
132 token.authorized_ok(HttpResponse::Ok().finish())
133 } else {
134 Err(ControllerError::new(
135 ControllerErrorType::Unauthorized,
136 "Incorrect email or password.".to_string(),
137 None,
138 ))
139 }
140 } else {
141 Err(ControllerError::new(
142 ControllerErrorType::BadRequest,
143 "Cannot create a new account when signed in.".to_string(),
144 None,
145 ))
146 }
147}
148
149async fn handle_test_mode_signup(
150 conn: &mut PgConnection,
151 session: &Session,
152 user_details: &CreateAccountDetails,
153 app_conf: &ApplicationConfiguration,
154) -> ControllerResult<HttpResponse> {
155 assert!(
156 app_conf.test_mode,
157 "handle_test_mode_signup called outside test mode"
158 );
159
160 warn!("Handling signup in test mode. No real account is created.");
161
162 let user_id = models::users::insert(
163 conn,
164 PKeyPolicy::Generate,
165 &user_details.email,
166 Some(&user_details.first_name),
167 Some(&user_details.last_name),
168 )
169 .await
170 .map_err(|e| {
171 ControllerError::new(
172 ControllerErrorType::InternalServerError,
173 "Failed to insert test user.".to_string(),
174 Some(anyhow!(e)),
175 )
176 })?;
177
178 models::user_details::update_user_country(conn, user_id, &user_details.country).await?;
179 models::user_details::update_user_email_communication_consent(
180 conn,
181 user_id,
182 user_details.email_communication_consent,
183 )
184 .await?;
185
186 let user = models::users::get_by_email(conn, &user_details.email).await?;
187 authorization::remember(session, user)?;
188
189 let token = skip_authorize();
190 token.authorized_ok(HttpResponse::Ok().finish())
191}
192
193#[instrument(skip(pool, payload,))]
199pub async fn authorize_multiple_actions_on_resources(
200 pool: web::Data<PgPool>,
201 user: Option<AuthUser>,
202 payload: web::Json<Vec<ActionOnResource>>,
203) -> ControllerResult<web::Json<Vec<bool>>> {
204 let mut conn = pool.acquire().await?;
205 let input = payload.into_inner();
206 let mut results = Vec::with_capacity(input.len());
207 if let Some(user) = user {
208 let user_roles = models::roles::get_roles(&mut conn, user.id).await?;
210
211 for action_on_resource in input {
212 if (authorize_with_fetched_list_of_roles(
213 &mut conn,
214 action_on_resource.action,
215 Some(user.id),
216 action_on_resource.resource,
217 &user_roles,
218 )
219 .await)
220 .is_ok()
221 {
222 results.push(true);
223 } else {
224 results.push(false);
225 }
226 }
227 } else {
228 for _action_on_resource in input {
230 results.push(false);
231 }
232 }
233 let token = skip_authorize();
234 token.authorized_ok(web::Json(results))
235}
236
237#[instrument(skip(session, pool, client, payload, app_conf))]
242pub async fn login(
243 session: Session,
244 pool: web::Data<PgPool>,
245 client: web::Data<OAuthClient>,
246 app_conf: web::Data<ApplicationConfiguration>,
247 payload: web::Json<Login>,
248) -> ControllerResult<web::Json<bool>> {
249 let mut conn = pool.acquire().await?;
250 let Login { email, password } = payload.into_inner();
251
252 if app_conf.development_uuid_login {
253 warn!("Trying development mode UUID login");
254 if let Ok(id) = Uuid::parse_str(&email) {
255 let user = { models::users::get_by_id(&mut conn, id).await? };
256 let token = skip_authorize();
257 authorization::remember(&session, user)?;
258 return token.authorized_ok(web::Json(true));
259 };
260 }
261
262 let success = if app_conf.test_mode {
263 warn!("Using test credentials. Normal accounts won't work.");
264 let success =
265 authorization::authenticate_test_user(&mut conn, &email, &password, &app_conf)
266 .await
267 .map_err(|e| {
268 ControllerError::new(
269 ControllerErrorType::Unauthorized,
270 "Could not find the test user. Have you seeded the database?".to_string(),
271 e,
272 )
273 })?;
274 if success {
275 let user = models::users::get_by_email(&mut conn, &email).await?;
276 authorization::remember(&session, user)?;
277 }
278 success
279 } else {
280 let auth_result =
281 authorization::authenticate_moocfi_user(&mut conn, &client, email, password).await?;
282
283 if let Some((user, _token)) = auth_result {
284 authorization::remember(&session, user)?;
285 true
286 } else {
287 false
288 }
289 };
290
291 if success {
292 info!("Authentication successful");
293 } else {
294 warn!("Authentication failed");
295 }
296
297 let token = skip_authorize();
298 token.authorized_ok(web::Json(success))
299}
300
301#[instrument(skip(session))]
305#[allow(clippy::async_yields_async)]
306pub async fn logout(session: Session) -> HttpResponse {
307 authorization::forget(&session);
308 HttpResponse::Ok().finish()
309}
310
311#[instrument(skip(session))]
315pub async fn logged_in(session: Session, pool: web::Data<PgPool>) -> web::Json<bool> {
316 let logged_in = authorization::has_auth_user_session(&session, pool).await;
317 web::Json(logged_in)
318}
319
320#[derive(Debug, Serialize)]
324#[cfg_attr(feature = "ts_rs", derive(TS))]
325pub struct UserInfo {
326 pub user_id: Uuid,
327 pub first_name: Option<String>,
328 pub last_name: Option<String>,
329}
330
331#[instrument(skip(auth_user, pool))]
336pub async fn user_info(
337 auth_user: Option<AuthUser>,
338 pool: web::Data<PgPool>,
339) -> ControllerResult<web::Json<Option<UserInfo>>> {
340 let token = skip_authorize();
341 if let Some(auth_user) = auth_user {
342 let mut conn = pool.acquire().await?;
343 let user_details =
344 models::user_details::get_user_details_by_user_id(&mut conn, auth_user.id).await?;
345
346 token.authorized_ok(web::Json(Some(UserInfo {
347 user_id: user_details.user_id,
348 first_name: user_details.first_name,
349 last_name: user_details.last_name,
350 })))
351 } else {
352 token.authorized_ok(web::Json(None))
353 }
354}
355
356pub async fn post_new_user_to_moocfi(
360 user_details: &CreateAccountDetails,
361 tmc_client: web::Data<TmcClient>,
362 app_conf: &ApplicationConfiguration,
363) -> anyhow::Result<()> {
364 tmc_client
365 .post_new_user_to_moocfi(
366 NewUserInfo {
367 first_name: user_details.first_name.clone(),
368 last_name: user_details.last_name.clone(),
369 email: user_details.email.clone(),
370 password: user_details.password.clone(),
371 password_confirmation: user_details.password_confirmation.clone(),
372 language: user_details.language.clone(),
373 },
374 app_conf,
375 )
376 .await
377}
378
379pub async fn update_user_information_to_tmc(
380 first_name: String,
381 last_name: String,
382 email: Option<String>,
383 user_upstream_id: String,
384 tmc_client: web::Data<TmcClient>,
385 app_conf: web::Data<ApplicationConfiguration>,
386) -> Result<(), Error> {
387 if app_conf.test_mode {
388 return Ok(());
389 }
390 tmc_client
391 .update_user_information(first_name, last_name, email, user_upstream_id)
392 .await
393 .map_err(|e| {
394 log::warn!("TMC user update failed: {:?}", e);
395 anyhow::anyhow!("TMC user update failed: {}", e)
396 })?;
397 Ok(())
398}
399
400pub async fn is_user_global_admin(conn: &mut PgConnection, user_id: Uuid) -> ModelResult<bool> {
401 let roles = models::roles::get_roles(conn, user_id).await?;
402 Ok(roles
403 .iter()
404 .any(|r| r.role == models::roles::UserRole::Admin && r.is_global))
405}
406
407pub fn _add_routes(cfg: &mut ServiceConfig) {
408 cfg.service(
409 web::resource("/signup")
410 .wrap(build_rate_limiting_middleware(Duration::from_secs(60), 15))
411 .wrap(build_rate_limiting_middleware(
412 Duration::from_secs(60 * 60 * 24),
413 1000,
414 ))
415 .to(signup),
416 )
417 .service(
418 web::resource("/login")
419 .wrap(build_rate_limiting_middleware(Duration::from_secs(60), 20))
420 .wrap(build_rate_limiting_middleware(
421 Duration::from_secs(60 * 60),
422 100,
423 ))
424 .wrap(build_rate_limiting_middleware(
425 Duration::from_secs(60 * 60 * 24),
426 500,
427 ))
428 .to(login),
429 )
430 .route("/logout", web::post().to(logout))
431 .route("/logged-in", web::get().to(logged_in))
432 .route("/authorize", web::post().to(authorize_action_on_resource))
433 .route(
434 "/authorize-multiple",
435 web::post().to(authorize_multiple_actions_on_resources),
436 )
437 .route("/user-info", web::get().to(user_info));
438}