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::{RateLimit, RateLimitConfig},
12 },
13 prelude::*,
14};
15use actix_session::Session;
16use anyhow::Error;
17use anyhow::anyhow;
18use headless_lms_models::ModelResult;
19use headless_lms_models::{
20 email_templates::EmailTemplateType, email_verification_tokens, user_email_codes,
21 user_passwords, users,
22};
23use headless_lms_utils::{
24 prelude::UtilErrorType,
25 tmc::{NewUserInfo, TmcClient},
26};
27use rand::Rng;
28use secrecy::SecretString;
29use tracing_log::log;
30
31#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
32#[cfg_attr(feature = "ts_rs", derive(TS))]
33pub struct Login {
34 email: String,
35 password: String,
36}
37
38#[derive(Debug, Serialize, Deserialize)]
39#[cfg_attr(feature = "ts_rs", derive(TS))]
40#[serde(tag = "type", rename_all = "snake_case")]
41pub enum LoginResponse {
42 Success,
43 RequiresEmailVerification { email_verification_token: String },
44 Failed,
45}
46
47#[instrument(skip(pool, payload,))]
52pub async fn authorize_action_on_resource(
53 pool: web::Data<PgPool>,
54 user: Option<AuthUser>,
55 payload: web::Json<ActionOnResource>,
56) -> ControllerResult<web::Json<bool>> {
57 let mut conn = pool.acquire().await?;
58 let data = payload.0;
59 if let Some(user) = user {
60 match authorize(&mut conn, data.action, Some(user.id), data.resource).await {
61 Ok(true_token) => true_token.authorized_ok(web::Json(true)),
62 _ => {
63 let false_token = skip_authorize();
65 false_token.authorized_ok(web::Json(false))
66 }
67 }
68 } else {
69 let false_token = skip_authorize();
71 false_token.authorized_ok(web::Json(false))
72 }
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76#[cfg_attr(feature = "ts_rs", derive(TS))]
77pub struct CreateAccountDetails {
78 pub email: String,
79 pub first_name: String,
80 pub last_name: String,
81 pub language: String,
82 pub password: String,
83 pub password_confirmation: String,
84 pub country: String,
85 pub email_communication_consent: bool,
86}
87
88#[instrument(skip(session, pool, payload, app_conf))]
109pub async fn signup(
110 session: Session,
111 payload: web::Json<CreateAccountDetails>,
112 pool: web::Data<PgPool>,
113 user: Option<AuthUser>,
114 app_conf: web::Data<ApplicationConfiguration>,
115 tmc_client: web::Data<TmcClient>,
116) -> ControllerResult<HttpResponse> {
117 let user_details = payload.0;
118 let mut conn = pool.acquire().await?;
119
120 if app_conf.test_mode {
121 return handle_test_mode_signup(&mut conn, &session, &user_details, &app_conf).await;
122 }
123 if user.is_none() {
124 let upstream_id = tmc_client
125 .post_new_user_to_tmc(
126 NewUserInfo {
127 first_name: user_details.first_name.clone(),
128 last_name: user_details.last_name.clone(),
129 email: user_details.email.clone(),
130 password: user_details.password.clone(),
131 password_confirmation: user_details.password_confirmation.clone(),
132 language: user_details.language.clone(),
133 },
134 app_conf.as_ref(),
135 )
136 .await
137 .map_err(|e| {
138 let error_message = e.message().to_string();
139 let error_type = match e.error_type() {
140 UtilErrorType::TmcHttpError => ControllerErrorType::InternalServerError,
141 UtilErrorType::TmcErrorResponse => ControllerErrorType::BadRequest,
142 _ => ControllerErrorType::InternalServerError,
143 };
144 ControllerError::new(error_type, error_message, Some(anyhow!(e)))
145 })?;
146 let password_secret = SecretString::new(user_details.password.into());
147
148 let user = models::users::insert_with_upstream_id_and_moocfi_id(
149 &mut conn,
150 &user_details.email,
151 Some(&user_details.first_name),
152 Some(&user_details.last_name),
153 upstream_id,
154 PKeyPolicy::Generate.into_uuid(),
155 )
156 .await
157 .map_err(|e| {
158 ControllerError::new(
159 ControllerErrorType::InternalServerError,
160 "Failed to insert user.".to_string(),
161 Some(anyhow!(e)),
162 )
163 })?;
164
165 let country = user_details.country.clone();
166 models::user_details::update_user_country(&mut conn, user.id, &country).await?;
167 models::user_details::update_user_email_communication_consent(
168 &mut conn,
169 user.id,
170 user_details.email_communication_consent,
171 )
172 .await?;
173
174 let password_hash = models::user_passwords::hash_password(&password_secret)
176 .map_err(|e| anyhow!("Failed to hash password: {:?}", e))?;
177
178 models::user_passwords::upsert_user_password(&mut conn, user.id, &password_hash)
179 .await
180 .map_err(|e| {
181 ControllerError::new(
182 ControllerErrorType::InternalServerError,
183 "Failed to add password to database".to_string(),
184 anyhow!(e),
185 )
186 })?;
187
188 tmc_client
190 .set_user_password_managed_by_courses_mooc_fi(upstream_id.to_string(), user.id)
191 .await
192 .map_err(|e| {
193 ControllerError::new(
194 ControllerErrorType::InternalServerError,
195 "Failed to notify TMC that user's password is saved in courses.mooc.fi"
196 .to_string(),
197 anyhow!(e),
198 )
199 })?;
200
201 let token = skip_authorize();
202 authorization::remember(&session, user)?;
203 token.authorized_ok(HttpResponse::Ok().finish())
204 } else {
205 Err(ControllerError::new(
206 ControllerErrorType::BadRequest,
207 "Cannot create a new account when signed in.".to_string(),
208 None,
209 ))
210 }
211}
212
213async fn handle_test_mode_signup(
214 conn: &mut PgConnection,
215 session: &Session,
216 user_details: &CreateAccountDetails,
217 app_conf: &ApplicationConfiguration,
218) -> ControllerResult<HttpResponse> {
219 assert!(
220 app_conf.test_mode,
221 "handle_test_mode_signup called outside test mode"
222 );
223
224 warn!("Handling signup in test mode. No real account is created.");
225
226 let user_id = models::users::insert(
227 conn,
228 PKeyPolicy::Generate,
229 &user_details.email,
230 Some(&user_details.first_name),
231 Some(&user_details.last_name),
232 )
233 .await
234 .map_err(|e| {
235 ControllerError::new(
236 ControllerErrorType::InternalServerError,
237 "Failed to insert test user.".to_string(),
238 Some(anyhow!(e)),
239 )
240 })?;
241
242 models::user_details::update_user_country(conn, user_id, &user_details.country).await?;
243 models::user_details::update_user_email_communication_consent(
244 conn,
245 user_id,
246 user_details.email_communication_consent,
247 )
248 .await?;
249
250 let user = models::users::get_by_email(conn, &user_details.email).await?;
251
252 let password_hash = models::user_passwords::hash_password(&SecretString::new(
253 user_details.password.clone().into(),
254 ))
255 .map_err(|e| anyhow!("Failed to hash password: {:?}", e))?;
256
257 models::user_passwords::upsert_user_password(conn, user.id, &password_hash)
258 .await
259 .map_err(|e| {
260 ControllerError::new(
261 ControllerErrorType::InternalServerError,
262 "Failed to add password to database".to_string(),
263 anyhow!(e),
264 )
265 })?;
266 authorization::remember(session, user)?;
267
268 let token = skip_authorize();
269 token.authorized_ok(HttpResponse::Ok().finish())
270}
271
272#[instrument(skip(pool, payload,))]
278pub async fn authorize_multiple_actions_on_resources(
279 pool: web::Data<PgPool>,
280 user: Option<AuthUser>,
281 payload: web::Json<Vec<ActionOnResource>>,
282) -> ControllerResult<web::Json<Vec<bool>>> {
283 let mut conn = pool.acquire().await?;
284 let input = payload.into_inner();
285 let mut results = Vec::with_capacity(input.len());
286 if let Some(user) = user {
287 let user_roles = models::roles::get_roles(&mut conn, user.id).await?;
289
290 for action_on_resource in input {
291 if (authorize_with_fetched_list_of_roles(
292 &mut conn,
293 action_on_resource.action,
294 Some(user.id),
295 action_on_resource.resource,
296 &user_roles,
297 )
298 .await)
299 .is_ok()
300 {
301 results.push(true);
302 } else {
303 results.push(false);
304 }
305 }
306 } else {
307 for _action_on_resource in input {
309 results.push(false);
310 }
311 }
312 let token = skip_authorize();
313 token.authorized_ok(web::Json(results))
314}
315
316#[instrument(skip(session, pool, client, payload, app_conf, tmc_client))]
321pub async fn login(
322 session: Session,
323 pool: web::Data<PgPool>,
324 client: web::Data<OAuthClient>,
325 app_conf: web::Data<ApplicationConfiguration>,
326 payload: web::Json<Login>,
327 tmc_client: web::Data<TmcClient>,
328) -> ControllerResult<web::Json<LoginResponse>> {
329 let mut conn = pool.acquire().await?;
330 let Login { email, password } = payload.into_inner();
331
332 if app_conf.development_uuid_login {
334 return handle_uuid_login(&session, &mut conn, &email, &app_conf).await;
335 }
336
337 if app_conf.test_mode {
339 return handle_test_mode_login(&session, &mut conn, &email, &password, &app_conf).await;
340 };
341
342 return handle_production_login(
343 &session,
344 &mut conn,
345 &client,
346 &tmc_client,
347 &email,
348 &password,
349 &app_conf,
350 )
351 .await;
352}
353
354async fn handle_uuid_login(
355 session: &Session,
356 conn: &mut PgConnection,
357 email: &str,
358 app_conf: &ApplicationConfiguration,
359) -> ControllerResult<web::Json<LoginResponse>> {
360 warn!("Trying development mode UUID login");
361 let token = skip_authorize();
362
363 if let Ok(id) = Uuid::parse_str(email) {
364 let user = { models::users::get_by_id(conn, id).await? };
365 let is_admin = is_user_global_admin(conn, user.id).await?;
366
367 if app_conf.enable_admin_email_verification && is_admin {
368 return handle_email_verification(conn, &user).await;
369 }
370
371 authorization::remember(session, user)?;
372 token.authorized_ok(web::Json(LoginResponse::Success))
373 } else {
374 warn!("Authentication failed");
375 token.authorized_ok(web::Json(LoginResponse::Failed))
376 }
377}
378
379async fn handle_test_mode_login(
380 session: &Session,
381 conn: &mut PgConnection,
382 email: &str,
383 password: &str,
384 app_conf: &ApplicationConfiguration,
385) -> ControllerResult<web::Json<LoginResponse>> {
386 warn!("Using test credentials. Normal accounts won't work.");
387
388 let user = match models::users::get_by_email(conn, email).await {
389 Ok(u) => u,
390 Err(_) => {
391 warn!("Test user not found for {}", email);
392 let token = skip_authorize();
393 return token.authorized_ok(web::Json(LoginResponse::Failed));
394 }
395 };
396
397 let mut is_authenticated =
398 authorization::authenticate_test_user(conn, email, password, app_conf)
399 .await
400 .map_err(|e| {
401 ControllerError::new(
402 ControllerErrorType::Unauthorized,
403 "Could not find the test user. Have you seeded the database?".to_string(),
404 e,
405 )
406 })?;
407
408 if !is_authenticated {
409 is_authenticated = models::user_passwords::verify_user_password(
410 conn,
411 user.id,
412 &SecretString::new(password.into()),
413 )
414 .await?;
415 }
416
417 if is_authenticated {
418 info!("Authentication successful");
419 let is_admin = is_user_global_admin(conn, user.id).await?;
420
421 if app_conf.enable_admin_email_verification && is_admin {
422 return handle_email_verification(conn, &user).await;
423 }
424
425 authorization::remember(session, user)?;
426 } else {
427 warn!("Authentication failed");
428 }
429
430 let token = skip_authorize();
431 if is_authenticated {
432 token.authorized_ok(web::Json(LoginResponse::Success))
433 } else {
434 token.authorized_ok(web::Json(LoginResponse::Failed))
435 }
436}
437
438async fn handle_production_login(
439 session: &Session,
440 conn: &mut PgConnection,
441 client: &OAuthClient,
442 tmc_client: &TmcClient,
443 email: &str,
444 password: &str,
445 app_conf: &ApplicationConfiguration,
446) -> ControllerResult<web::Json<LoginResponse>> {
447 let mut is_authenticated = false;
448 let mut authenticated_user: Option<headless_lms_models::users::User> = None;
449
450 if let Ok(user) = models::users::get_by_email(conn, email).await {
452 let is_password_stored =
453 models::user_passwords::check_if_users_password_is_stored(conn, user.id).await?;
454 if is_password_stored {
455 is_authenticated = models::user_passwords::verify_user_password(
456 conn,
457 user.id,
458 &SecretString::new(password.into()),
459 )
460 .await?;
461
462 if is_authenticated {
463 info!("Authentication successful");
464 authenticated_user = Some(user);
465 }
466 }
467 }
468
469 if !is_authenticated {
471 let auth_result = authorization::authenticate_moocfi_user(
472 conn,
473 client,
474 email.to_string(),
475 password.to_string(),
476 tmc_client,
477 )
478 .await?;
479
480 if let Some((user, _token)) = auth_result {
481 let password_hash =
483 models::user_passwords::hash_password(&SecretString::new(password.into()))
484 .map_err(|e| anyhow!("Failed to hash password: {:?}", e))?;
485
486 models::user_passwords::upsert_user_password(conn, user.id, &password_hash)
487 .await
488 .map_err(|e| {
489 ControllerError::new(
490 ControllerErrorType::InternalServerError,
491 "Failed to add password to database".to_string(),
492 anyhow!(e),
493 )
494 })?;
495
496 if let Some(upstream_id) = user.upstream_id {
498 tmc_client
499 .set_user_password_managed_by_courses_mooc_fi(upstream_id.to_string(), user.id)
500 .await
501 .map_err(|e| {
502 ControllerError::new(
503 ControllerErrorType::InternalServerError,
504 "Failed to notify TMC that users password is saved in courses.mooc.fi"
505 .to_string(),
506 anyhow!(e),
507 )
508 })?;
509 } else {
510 warn!("User has no upstream_id; skipping notify to TMC");
511 }
512 info!("Authentication successful");
513 authenticated_user = Some(user);
514 is_authenticated = true;
515 }
516 }
517
518 let token = skip_authorize();
519 if is_authenticated {
520 if let Some(user) = authenticated_user {
521 let is_admin = is_user_global_admin(conn, user.id).await?;
522
523 if app_conf.enable_admin_email_verification && is_admin {
524 return handle_email_verification(conn, &user).await;
525 }
526
527 authorization::remember(session, user)?;
528 }
529 token.authorized_ok(web::Json(LoginResponse::Success))
530 } else {
531 warn!("Authentication failed");
532 token.authorized_ok(web::Json(LoginResponse::Failed))
533 }
534}
535
536#[instrument(skip(session))]
540#[allow(clippy::async_yields_async)]
541pub async fn logout(session: Session) -> HttpResponse {
542 authorization::forget(&session);
543 HttpResponse::Ok().finish()
544}
545
546#[instrument(skip(session))]
550pub async fn logged_in(session: Session, pool: web::Data<PgPool>) -> web::Json<bool> {
551 let logged_in = authorization::has_auth_user_session(&session, pool).await;
552 web::Json(logged_in)
553}
554
555#[derive(Debug, Serialize)]
559#[cfg_attr(feature = "ts_rs", derive(TS))]
560pub struct UserInfo {
561 pub user_id: Uuid,
562 pub first_name: Option<String>,
563 pub last_name: Option<String>,
564}
565
566#[instrument(skip(auth_user, pool))]
571pub async fn user_info(
572 auth_user: Option<AuthUser>,
573 pool: web::Data<PgPool>,
574) -> ControllerResult<web::Json<Option<UserInfo>>> {
575 let token = skip_authorize();
576 if let Some(auth_user) = auth_user {
577 let mut conn = pool.acquire().await?;
578 let user_details =
579 models::user_details::get_user_details_by_user_id(&mut conn, auth_user.id).await?;
580
581 token.authorized_ok(web::Json(Some(UserInfo {
582 user_id: user_details.user_id,
583 first_name: user_details.first_name,
584 last_name: user_details.last_name,
585 })))
586 } else {
587 token.authorized_ok(web::Json(None))
588 }
589}
590
591#[derive(Debug, Serialize, Deserialize)]
592#[cfg_attr(feature = "ts_rs", derive(TS))]
593pub struct SendEmailCodeData {
594 pub email: String,
595 pub password: String,
596 pub language: String,
597}
598
599#[instrument(skip(pool, payload, auth_user))]
603#[allow(clippy::async_yields_async)]
604pub async fn send_delete_user_email_code(
605 auth_user: Option<AuthUser>,
606 pool: web::Data<PgPool>,
607 payload: web::Json<SendEmailCodeData>,
608) -> ControllerResult<web::Json<bool>> {
609 let token = skip_authorize();
610
611 if let Some(auth_user) = auth_user {
613 let mut conn = pool.acquire().await?;
614
615 let password_ok = user_passwords::verify_user_password(
616 &mut conn,
617 auth_user.id,
618 &SecretString::new(payload.password.clone().into()),
619 )
620 .await?;
621
622 if !password_ok {
623 info!(
624 "User {} attempted account deletion with incorrect password",
625 auth_user.id
626 );
627
628 return token.authorized_ok(web::Json(false));
629 }
630
631 let language = &payload.language;
632
633 let delete_template = models::email_templates::get_generic_email_template_by_type_and_language(
635 &mut conn,
636 EmailTemplateType::DeleteUserEmail,
637 language,
638 )
639 .await
640 .map_err(|_e| {
641 anyhow::anyhow!(
642 "Account deletion email template not configured. Missing template 'delete-user-email' for language '{}'",
643 language
644 )
645 })?;
646
647 let user = models::users::get_by_id(&mut conn, auth_user.id).await?;
648
649 let code = if let Some(existing) =
650 models::user_email_codes::get_unused_user_email_code_with_user_id(
651 &mut conn,
652 auth_user.id,
653 )
654 .await?
655 {
656 existing.code
657 } else {
658 let new_code: String = rand::rng().random_range(100_000..1_000_000).to_string();
659 models::user_email_codes::insert_user_email_code(
660 &mut conn,
661 auth_user.id,
662 new_code.clone(),
663 )
664 .await?;
665 new_code
666 };
667
668 models::user_email_codes::insert_user_email_code(&mut conn, auth_user.id, code.clone())
669 .await?;
670 let _ =
671 models::email_deliveries::insert_email_delivery(&mut conn, user.id, delete_template.id)
672 .await?;
673
674 return token.authorized_ok(web::Json(true));
675 }
676 token.authorized_ok(web::Json(false))
677}
678
679#[derive(Debug, Serialize, Deserialize)]
680#[cfg_attr(feature = "ts_rs", derive(TS))]
681pub struct EmailCode {
682 pub code: String,
683}
684
685#[instrument(skip(pool, payload, auth_user, session))]
689#[allow(clippy::async_yields_async)]
690pub async fn delete_user_account(
691 auth_user: Option<AuthUser>,
692 pool: web::Data<PgPool>,
693 payload: web::Json<EmailCode>,
694 session: Session,
695 tmc_client: web::Data<TmcClient>,
696) -> ControllerResult<web::Json<bool>> {
697 let token = skip_authorize();
698 if let Some(auth_user) = auth_user {
699 let mut conn = pool.acquire().await?;
700
701 let code_ok = user_email_codes::is_reset_user_email_code_valid(
703 &mut conn,
704 auth_user.id,
705 &payload.code,
706 )
707 .await?;
708
709 if !code_ok {
710 info!(
711 "User {} attempted account deletion with incorrect code",
712 auth_user.id
713 );
714 return token.authorized_ok(web::Json(false));
715 }
716
717 let mut tx = conn.begin().await?;
718 let user = users::get_by_id(&mut tx, auth_user.id).await?;
719
720 if let Some(upstream_id) = user.upstream_id {
722 let upstream_id_str = upstream_id.to_string();
723 let tmc_success = tmc_client
724 .delete_user_from_tmc(upstream_id_str)
725 .await
726 .unwrap_or(false);
727
728 if !tmc_success {
729 info!("TMC deletion failed for user {}", auth_user.id);
730 return token.authorized_ok(web::Json(false));
731 }
732 }
733
734 users::delete_user(&mut tx, auth_user.id).await?;
736 user_email_codes::mark_user_email_code_used(&mut tx, auth_user.id, &payload.code).await?;
737
738 tx.commit().await?;
739
740 authorization::forget(&session);
741 token.authorized_ok(web::Json(true))
742 } else {
743 return token.authorized_ok(web::Json(false));
744 }
745}
746
747pub async fn update_user_information_to_tmc(
748 first_name: String,
749 last_name: String,
750 email: Option<String>,
751 user_upstream_id: String,
752 tmc_client: web::Data<TmcClient>,
753 app_conf: web::Data<ApplicationConfiguration>,
754) -> Result<(), Error> {
755 if app_conf.test_mode {
756 return Ok(());
757 }
758 tmc_client
759 .update_user_information(first_name, last_name, email, user_upstream_id)
760 .await
761 .map_err(|e| {
762 log::warn!("TMC user update failed: {:?}", e);
763 anyhow::anyhow!("TMC user update failed: {}", e)
764 })?;
765 Ok(())
766}
767
768pub async fn is_user_global_admin(conn: &mut PgConnection, user_id: Uuid) -> ModelResult<bool> {
769 let roles = models::roles::get_roles(conn, user_id).await?;
770 Ok(roles
771 .iter()
772 .any(|r| r.role == models::roles::UserRole::Admin && r.is_global))
773}
774
775async fn handle_email_verification(
776 conn: &mut PgConnection,
777 user: &headless_lms_models::users::User,
778) -> ControllerResult<web::Json<LoginResponse>> {
779 let code: String = rand::rng().random_range(100_000..1_000_000).to_string();
780
781 let email_verification_token =
782 email_verification_tokens::create_email_verification_token(conn, user.id, code.clone())
783 .await
784 .map_err(|e| {
785 ControllerError::new(
786 ControllerErrorType::InternalServerError,
787 "Failed to create email verification token".to_string(),
788 Some(anyhow!(e)),
789 )
790 })?;
791
792 user_email_codes::insert_user_email_code(conn, user.id, code.clone())
793 .await
794 .map_err(|e| {
795 ControllerError::new(
796 ControllerErrorType::InternalServerError,
797 "Failed to insert user email code".to_string(),
798 Some(anyhow!(e)),
799 )
800 })?;
801
802 let email_template = models::email_templates::get_generic_email_template_by_type_and_language(
803 conn,
804 EmailTemplateType::ConfirmEmailCode,
805 "en",
806 )
807 .await
808 .map_err(|e| {
809 ControllerError::new(
810 ControllerErrorType::InternalServerError,
811 format!("Failed to get email template: {}", e.message()),
812 Some(anyhow!(e)),
813 )
814 })?;
815
816 models::email_deliveries::insert_email_delivery(conn, user.id, email_template.id)
817 .await
818 .map_err(|e| {
819 ControllerError::new(
820 ControllerErrorType::InternalServerError,
821 "Failed to insert email delivery".to_string(),
822 Some(anyhow!(e)),
823 )
824 })?;
825
826 email_verification_tokens::mark_code_sent(conn, &email_verification_token)
827 .await
828 .map_err(|e| {
829 ControllerError::new(
830 ControllerErrorType::InternalServerError,
831 "Failed to mark code as sent".to_string(),
832 Some(anyhow!(e)),
833 )
834 })?;
835
836 let token = skip_authorize();
837 token.authorized_ok(web::Json(LoginResponse::RequiresEmailVerification {
838 email_verification_token,
839 }))
840}
841
842#[derive(Debug, Serialize, Deserialize)]
843#[cfg_attr(feature = "ts_rs", derive(TS))]
844pub struct VerifyEmailRequest {
845 pub email_verification_token: String,
846 pub code: String,
847}
848
849#[instrument(skip(session, pool, payload))]
853pub async fn verify_email(
854 session: Session,
855 pool: web::Data<PgPool>,
856 payload: web::Json<VerifyEmailRequest>,
857) -> ControllerResult<web::Json<bool>> {
858 let mut conn = pool.acquire().await?;
859 let payload = payload.into_inner();
860
861 let token = email_verification_tokens::get_by_email_verification_token(
862 &mut conn,
863 &payload.email_verification_token,
864 )
865 .await
866 .map_err(|e| {
867 ControllerError::new(
868 ControllerErrorType::InternalServerError,
869 "Failed to get email verification token".to_string(),
870 Some(anyhow!(e)),
871 )
872 })?;
873
874 let Some(token_value) = token else {
875 let skip_token = skip_authorize();
876 return skip_token.authorized_ok(web::Json(false));
877 };
878
879 let is_valid = email_verification_tokens::verify_code(
880 &mut conn,
881 &payload.email_verification_token,
882 &payload.code,
883 )
884 .await
885 .map_err(|e| {
886 ControllerError::new(
887 ControllerErrorType::InternalServerError,
888 "Failed to verify code".to_string(),
889 Some(anyhow!(e)),
890 )
891 })?;
892
893 if !is_valid {
894 let skip_token = skip_authorize();
895 return skip_token.authorized_ok(web::Json(false));
896 }
897
898 let user_id = token_value.user_id;
899
900 user_email_codes::mark_user_email_code_used(&mut conn, user_id, &payload.code)
901 .await
902 .map_err(|e| {
903 ControllerError::new(
904 ControllerErrorType::InternalServerError,
905 "Failed to mark user email code as used".to_string(),
906 Some(anyhow!(e)),
907 )
908 })?;
909
910 email_verification_tokens::mark_as_used(&mut conn, &payload.email_verification_token)
911 .await
912 .map_err(|e| {
913 ControllerError::new(
914 ControllerErrorType::InternalServerError,
915 "Failed to mark token as used".to_string(),
916 Some(anyhow!(e)),
917 )
918 })?;
919
920 let user = models::users::get_by_id(&mut conn, user_id)
921 .await
922 .map_err(|e| {
923 ControllerError::new(
924 ControllerErrorType::InternalServerError,
925 "Failed to get user".to_string(),
926 Some(anyhow!(e)),
927 )
928 })?;
929
930 authorization::remember(&session, user)?;
931
932 let skip_token = skip_authorize();
933 skip_token.authorized_ok(web::Json(true))
934}
935
936pub fn _add_routes(cfg: &mut ServiceConfig) {
937 cfg.service(
938 web::resource("/signup")
939 .wrap(RateLimit::new(RateLimitConfig {
940 per_minute: Some(15),
941 per_hour: None,
942 per_day: Some(1000),
943 per_month: None,
944 }))
945 .to(signup),
946 )
947 .service(
948 web::resource("/login")
949 .wrap(RateLimit::new(RateLimitConfig {
950 per_minute: Some(20),
951 per_hour: Some(100),
952 per_day: Some(500),
953 per_month: None,
954 }))
955 .to(login),
956 )
957 .route("/logout", web::post().to(logout))
958 .route("/logged-in", web::get().to(logged_in))
959 .route("/authorize", web::post().to(authorize_action_on_resource))
960 .route(
961 "/authorize-multiple",
962 web::post().to(authorize_multiple_actions_on_resources),
963 )
964 .route("/user-info", web::get().to(user_info))
965 .service(
966 web::resource("/delete-user-account")
967 .wrap(RateLimit::new(RateLimitConfig {
968 per_minute: None,
969 per_hour: Some(5),
970 per_day: Some(10),
971 per_month: None,
972 }))
973 .to(delete_user_account),
974 )
975 .service(
976 web::resource("/send-email-code")
977 .wrap(RateLimit::new(RateLimitConfig {
978 per_minute: None,
979 per_hour: Some(5),
980 per_day: Some(20),
981 per_month: None,
982 }))
983 .to(send_delete_user_email_code),
984 )
985 .service(
986 web::resource("/verify-email")
987 .wrap(RateLimit::new(RateLimitConfig {
988 per_minute: Some(10),
989 per_hour: Some(50),
990 per_day: None,
991 per_month: None,
992 }))
993 .to(verify_email),
994 );
995}