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