use crate::{
middleware::{
check_credentials_with_multi_identity_provider,
parse_issuer_from_request,
},
rest::shared::{build_actor_context, UrlGroup},
settings::ADMIN_API_SCOPE,
};
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
use chrono::Duration;
use myc_core::{
domain::{
actors::SystemActor,
dtos::user::{Totp, User},
entities::TokenInvalidation,
},
models::AccountLifeCycle,
settings::TEMPLATES,
use_cases::role_scoped::beginner::user::{
check_email_password_validity, check_token_and_activate_user,
check_token_and_reset_password, create_default_user,
request_magic_link, start_password_redefinition, totp_check_token,
totp_disable, totp_finish_activation, totp_start_activation,
verify_magic_link,
},
};
use myc_diesel::repositories::SqlAppModule;
use myc_http_tools::{
functions::encode_jwt, models::internal_auth_config::InternalOauthConfig,
responses::GatewayError, utils::HttpJsonResponse,
wrappers::default_response_to_http_response::handle_mapped_error, Email,
};
use mycelium_base::entities::FetchResponseKind;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize, Serializer};
use serde_json::json;
use shaku::HasComponent;
use tera::Context as TeraContext;
use tracing::warn;
use utoipa::{IntoParams, ToResponse, ToSchema};
pub fn configure(config: &mut web::ServiceConfig) {
config
.service(check_email_registration_status_url)
.service(create_default_user_url)
.service(check_user_token_url)
.service(start_password_redefinition_url)
.service(check_token_and_reset_password_url)
.service(check_email_password_validity_url)
.service(totp_start_activation_url)
.service(totp_finish_activation_url)
.service(totp_check_token_url)
.service(totp_disable_url)
.service(request_magic_link_url)
.service(display_magic_link_url)
.service(verify_magic_link_url);
}
#[derive(Serialize, Deserialize, ToResponse, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CheckEmailStatusResponse {
email: String,
status: String,
provider: Option<String>,
has_account: bool,
}
fn serialize_duration<S>(
duration: &Duration,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(duration.num_seconds() as u64)
}
#[derive(Serialize, ToResponse, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MyceliumLoginResponse {
token: String,
#[serde(serialize_with = "serialize_duration")]
duration: Duration,
totp_required: bool,
#[serde(flatten)]
user: User,
}
#[derive(Deserialize, ToSchema, IntoParams)]
#[serde(rename_all = "camelCase")]
pub struct TotpActivationStartedParams {
qr_code: Option<bool>,
}
#[derive(Serialize, ToResponse, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TotpActivationStartedResponse {
totp_url: Option<String>,
}
#[derive(Serialize, ToResponse, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TotpActivationFinishedResponse {
finished: bool,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TotpUpdatingValidationBody {
token: String,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MagicLinkRequestBody {
email: String,
}
#[derive(Serialize, ToResponse, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MagicLinkRequestResponse {
sent: bool,
}
#[derive(Deserialize, IntoParams, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MagicLinkDisplayParams {
token: String,
email: String,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MagicLinkVerifyBody {
email: String,
code: String,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateDefaultUserBody {
email: String,
first_name: Option<String>,
last_name: Option<String>,
password: Option<String>,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CheckTokenBody {
token: String,
email: String,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct StartPasswordResetBody {
email: String,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ResetPasswordBody {
token: String,
email: String,
new_password: String,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CheckUserCredentialsBody {
email: String,
password: String,
}
#[utoipa::path(
get,
operation_id = "check_email_registration_status",
responses(
(
status = 410,
description = "This endpoint is deprecated. Please use the /status endpoint instead.",
body = HttpJsonResponse,
),
),
security(()),
)]
#[get("/status")]
pub async fn check_email_registration_status_url() -> impl Responder {
HttpResponse::Gone().json(HttpJsonResponse::new_message(
"This endpoint is deprecated. Please use the /status endpoint instead.",
))
}
#[utoipa::path(
post,
operation_id = "create_default_user",
params(
(
"Authorization" = Option<String>,
Header,
description = "An optional Bearer token. When included, the user \
will be registered with the provider informed in the token.",
)
),
request_body = CreateDefaultUserBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 403,
description = "Forbidden.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Unauthorized.",
body = HttpJsonResponse,
),
(
status = 201,
description = "User successfully created.",
body = User,
),
),
security(()),
)]
#[post("")]
pub async fn create_default_user_url(
req: HttpRequest,
body: web::Json<CreateDefaultUserBody>,
life_cycle_settings: web::Data<AccountLifeCycle>,
sql_app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let provider = match parse_issuer_from_request(req.clone()).await {
Err(err) => match err {
GatewayError::Unauthorized(_) => None,
_ => {
warn!("Invalid issuer: {err}");
return HttpResponse::BadRequest().json(
HttpJsonResponse::new_message(
"Invalid issuer.".to_string(),
),
);
}
},
Ok((issuer, _)) => Some(issuer),
};
match create_default_user(
body.email.to_owned(),
body.first_name.to_owned(),
body.last_name.to_owned(),
body.password.to_owned(),
provider,
life_cycle_settings.get_ref().to_owned(),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
)
.await
{
Ok(_) => HttpResponse::Created()
.json(json!({"message": "User created successfully"})),
Err(err) => handle_mapped_error(err),
}
}
#[utoipa::path(
post,
operation_id = "check_user_token",
request_body = CheckTokenBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 403,
description = "Forbidden.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Unauthorized.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Activation token is valid.",
body = bool,
),
),
security(()),
)]
#[post("/validate-activation-token")]
pub async fn check_user_token_url(
body: web::Json<CheckTokenBody>,
app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let email = match Email::from_string(body.email.to_owned()) {
Err(err) => {
warn!("Invalid email: {}", err);
return HttpResponse::BadRequest().json(
HttpJsonResponse::new_message(
"Invalid email address.".to_string(),
),
);
}
Ok(email) => email,
};
match check_token_and_activate_user(
body.token.to_owned(),
email,
Box::new(&*app_module.resolve_ref()),
Box::new(&*app_module.resolve_ref()),
Box::new(&*app_module.resolve_ref()),
)
.await
{
Ok(_) => HttpResponse::Ok().finish(),
Err(err) => handle_mapped_error(err),
}
}
#[utoipa::path(
post,
operation_id = "start_password_redefinition",
request_body = StartPasswordResetBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 403,
description = "Forbidden.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Unauthorized.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Password change requested.",
body = bool,
),
),
security(()),
)]
#[post("/start-password-reset")]
pub async fn start_password_redefinition_url(
body: web::Json<StartPasswordResetBody>,
life_cycle_settings: web::Data<AccountLifeCycle>,
sql_app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let email = match Email::from_string(body.email.to_owned()) {
Err(err) => {
warn!("Invalid email: {}", err);
return HttpResponse::BadRequest().json(
HttpJsonResponse::new_message(
"Invalid email address.".to_string(),
),
);
}
Ok(email) => email,
};
match start_password_redefinition(
email,
life_cycle_settings.get_ref().to_owned(),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
)
.await
{
Ok(_) => HttpResponse::Ok().json(true),
Err(err) => handle_mapped_error(err),
}
}
#[utoipa::path(
post,
operation_id = "check_token_and_reset_password",
request_body = ResetPasswordBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 403,
description = "Forbidden.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Unauthorized.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Password change requested.",
body = bool,
),
),
security(()),
)]
#[post("/reset-password")]
pub async fn check_token_and_reset_password_url(
body: web::Json<ResetPasswordBody>,
life_cycle_settings: web::Data<AccountLifeCycle>,
sql_app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let email = match Email::from_string(body.email.to_owned()) {
Err(err) => {
warn!("Invalid email: {}", err);
return HttpResponse::BadRequest().json(
HttpJsonResponse::new_message(
"Invalid email address.".to_string(),
),
);
}
Ok(email) => email,
};
match check_token_and_reset_password(
body.token.to_owned(),
email,
body.new_password.to_owned(),
life_cycle_settings.get_ref().to_owned(),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
)
.await
{
Ok(_) => HttpResponse::Ok().json(true),
Err(err) => handle_mapped_error(err),
}
}
#[utoipa::path(
post,
operation_id = "login_with_email_and_password",
request_body = CheckUserCredentialsBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 403,
description = "Forbidden.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Unauthorized.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Credentials are valid.",
body = MyceliumLoginResponse,
),
),
security(()),
)]
#[post("/login")]
pub async fn check_email_password_validity_url(
body: web::Json<CheckUserCredentialsBody>,
app_module: web::Data<SqlAppModule>,
auth_config: web::Data<InternalOauthConfig>,
core_config: web::Data<AccountLifeCycle>,
) -> impl Responder {
let email_instance = match Email::from_string(body.email.to_owned()) {
Err(err) => {
warn!("Invalid email: {}", err);
return HttpResponse::BadRequest().json(
HttpJsonResponse::new_message(
"Invalid email address.".to_string(),
),
);
}
Ok(email) => email,
};
match check_email_password_validity(
email_instance,
body.password.to_owned(),
Box::new(&*app_module.resolve_ref()),
)
.await
{
Err(err) => handle_mapped_error(err),
Ok((valid, user)) => match valid {
true => {
let _user = if let Some(user) = user {
user
} else {
return HttpResponse::NoContent().finish();
};
match _user.mfa().totp {
Totp::Disabled | Totp::Unknown => match encode_jwt(
_user.to_owned(),
auth_config.get_ref().to_owned(),
core_config.get_ref().to_owned(),
false,
)
.await
{
Err(err) => return err,
Ok((token, duration)) => {
return HttpResponse::Ok().json(
MyceliumLoginResponse {
token,
duration,
totp_required: false,
user: _user,
},
)
}
},
Totp::Enabled { verified, .. } => {
if !verified {
return HttpResponse::TemporaryRedirect()
.append_header((
"Location",
format!(
"{}/totp/enable",
build_actor_context(
SystemActor::Beginner,
UrlGroup::Users
)
),
))
.finish();
}
match encode_jwt(
_user.to_owned(),
auth_config.get_ref().to_owned(),
core_config.get_ref().to_owned(),
true,
)
.await
{
Err(err) => return err,
Ok((token, duration)) => {
return HttpResponse::Ok().json(
MyceliumLoginResponse {
token,
duration,
totp_required: true,
user: _user,
},
)
}
}
}
}
}
false => HttpResponse::Unauthorized().finish(),
},
}
}
#[utoipa::path(
post,
operation_id = "totp_start_activation",
params(TotpActivationStartedParams),
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 403,
description = "Forbidden.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Unauthorized.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Totp Activation Started.",
body = TotpActivationStartedResponse,
),
(
status = 200,
description = "Totp Activation Started.",
body = String,
),
),
)]
#[post("/totp/enable")]
pub async fn totp_start_activation_url(
req: HttpRequest,
query: web::Query<TotpActivationStartedParams>,
life_cycle_settings: web::Data<AccountLifeCycle>,
sql_app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let (email, _) =
match check_credentials_with_multi_identity_provider(req).await {
Err(err) => {
warn!("err: {:?}", err);
return HttpResponse::InternalServerError()
.json(HttpJsonResponse::new_message(err));
}
Ok(res) => res,
};
let as_qr_code = query.qr_code.to_owned().unwrap_or(false);
match totp_start_activation(
email,
query.qr_code,
life_cycle_settings.get_ref().to_owned(),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
)
.await
{
Ok((totp_url, qr_code)) => {
if as_qr_code && qr_code.is_some() {
return HttpResponse::build(StatusCode::OK)
.content_type("image/jpeg")
.body(qr_code.unwrap());
};
HttpResponse::Ok().json(TotpActivationStartedResponse { totp_url })
}
Err(err) => handle_mapped_error(err),
}
}
#[utoipa::path(
post,
operation_id = "totp_finish_activation",
request_body = TotpUpdatingValidationBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 403,
description = "Forbidden.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Unauthorized.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Credentials are valid.",
body = MyceliumLoginResponse,
),
),
)]
#[post("/totp/validate-app")]
pub async fn totp_finish_activation_url(
req: HttpRequest,
body: web::Json<TotpUpdatingValidationBody>,
life_cycle_settings: web::Data<AccountLifeCycle>,
sql_app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let (email, _) =
match check_credentials_with_multi_identity_provider(req).await {
Err(err) => {
warn!("err: {:?}", err);
return HttpResponse::InternalServerError()
.json(HttpJsonResponse::new_message(err));
}
Ok(res) => res,
};
match totp_finish_activation(
email,
body.token.to_owned(),
life_cycle_settings.get_ref().to_owned(),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
)
.await
{
Ok(_) => HttpResponse::Ok()
.json(TotpActivationFinishedResponse { finished: true }),
Err(err) => handle_mapped_error(err),
}
}
#[utoipa::path(
post,
operation_id = "totp_check_token",
request_body = TotpUpdatingValidationBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 403,
description = "Forbidden.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Unauthorized.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Credentials are valid.",
body = MyceliumLoginResponse,
),
),
)]
#[post("/totp/check-token")]
pub async fn totp_check_token_url(
req: HttpRequest,
body: web::Json<TotpUpdatingValidationBody>,
auth_config: web::Data<InternalOauthConfig>,
life_cycle_settings: web::Data<AccountLifeCycle>,
app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let (email, _) =
match check_credentials_with_multi_identity_provider(req).await {
Err(err) => {
warn!("err: {:?}", err);
return HttpResponse::InternalServerError()
.json(HttpJsonResponse::new_message(err));
}
Ok(res) => res,
};
match totp_check_token(
email,
body.token.to_owned(),
life_cycle_settings.get_ref().to_owned(),
Box::new(&*app_module.resolve_ref()),
)
.await
{
Ok(res) => {
match encode_jwt(
res.to_owned(),
auth_config.get_ref().to_owned(),
life_cycle_settings.get_ref().to_owned(),
false,
)
.await
{
Err(err) => return err,
Ok((token, duration)) => {
return HttpResponse::Ok().json(MyceliumLoginResponse {
token,
duration,
totp_required: false,
user: res,
})
}
}
}
Err(err) => handle_mapped_error(err),
}
}
#[utoipa::path(
post,
operation_id = "totp_disable",
request_body = TotpUpdatingValidationBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 403,
description = "Forbidden.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Unauthorized.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Credentials are valid.",
body = MyceliumLoginResponse,
),
),
)]
#[post("/totp/disable")]
pub async fn totp_disable_url(
req: HttpRequest,
body: web::Json<TotpUpdatingValidationBody>,
life_cycle_settings: web::Data<AccountLifeCycle>,
sql_app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let (email, _) =
match check_credentials_with_multi_identity_provider(req).await {
Err(err) => {
warn!("err: {:?}", err);
return HttpResponse::InternalServerError()
.json(HttpJsonResponse::new_message(err));
}
Ok(res) => res,
};
match totp_disable(
email,
body.token.to_owned(),
life_cycle_settings.get_ref().to_owned(),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
)
.await
{
Ok(_) => HttpResponse::NoContent().finish(),
Err(err) => handle_mapped_error(err),
}
}
#[utoipa::path(
post,
operation_id = "request_magic_link",
request_body = MagicLinkRequestBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Magic link email dispatched.",
body = MagicLinkRequestResponse,
),
),
security(()),
)]
#[post("/magic-link/request")]
pub async fn request_magic_link_url(
body: web::Json<MagicLinkRequestBody>,
life_cycle_settings: web::Data<AccountLifeCycle>,
sql_app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let email = match Email::from_string(body.email.to_owned()) {
Err(err) => {
warn!("Invalid email: {}", err);
return HttpResponse::BadRequest().json(
HttpJsonResponse::new_message(
"Invalid email address.".to_string(),
),
);
}
Ok(email) => email,
};
let display_base_url = match life_cycle_settings.domain_url.clone() {
None => {
warn!("domain_url not configured; suppressing magic link request");
return HttpResponse::Ok()
.json(MagicLinkRequestResponse { sent: true });
}
Some(resolver) => match resolver.async_get_or_error().await {
Err(err) => {
warn!("domain_url resolution failed (suppressed): {:?}", err);
return HttpResponse::Ok()
.json(MagicLinkRequestResponse { sent: true });
}
Ok(url) => format!(
"{}/{}/{}/magic-link/display",
url.trim_end_matches('/'),
ADMIN_API_SCOPE,
build_actor_context(SystemActor::Beginner, UrlGroup::Users,),
),
},
};
if let Err(err) = request_magic_link(
email,
display_base_url,
life_cycle_settings.get_ref().to_owned(),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
Box::new(&*sql_app_module.resolve_ref()),
)
.await
{
warn!("request_magic_link error (suppressed): {:?}", err);
}
HttpResponse::Ok().json(MagicLinkRequestResponse { sent: true })
}
#[utoipa::path(
get,
operation_id = "display_magic_link",
params(MagicLinkDisplayParams),
responses(
(
status = 200,
description = "HTML page with the 6-digit code.",
content_type = "text/html",
),
(
status = 401,
description = "Link invalid or already used.",
content_type = "text/html",
),
),
security(()),
)]
#[get("/magic-link/display")]
pub async fn display_magic_link_url(
query: web::Query<MagicLinkDisplayParams>,
sql_app_module: web::Data<SqlAppModule>,
) -> impl Responder {
let email = match Email::from_string(query.email.to_owned()) {
Err(err) => {
warn!("Invalid email in magic link display: {}", err);
return render_magic_link_error_page();
}
Ok(email) => email,
};
let token_repo: &dyn TokenInvalidation = &*sql_app_module.resolve_ref();
let code = match token_repo
.get_code_and_invalidate_display_token(&email, &query.token)
.await
{
Ok(FetchResponseKind::Found(code)) => code,
_ => return render_magic_link_error_page(),
};
let mut context = TeraContext::new();
context.insert("code", &code);
context.insert("app_name", "Mycelium");
context.insert("expires_in_minutes", &15u32);
match TEMPLATES.render("web/magic-link-display.html", &context) {
Ok(html) => HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html),
Err(err) => {
warn!("Failed to render magic-link-display template: {}", err);
HttpResponse::InternalServerError().finish()
}
}
}
fn render_magic_link_error_page() -> HttpResponse {
let mut context = TeraContext::new();
context.insert("app_name", "Mycelium");
match TEMPLATES.render("web/magic-link-display-error.html", &context) {
Ok(html) => HttpResponse::Unauthorized()
.content_type("text/html; charset=utf-8")
.body(html),
Err(err) => {
warn!(
"Failed to render magic-link-display-error template: {}",
err
);
HttpResponse::Unauthorized().finish()
}
}
}
#[utoipa::path(
post,
operation_id = "verify_magic_link",
request_body = MagicLinkVerifyBody,
responses(
(
status = 500,
description = "Unknown internal server error.",
body = HttpJsonResponse,
),
(
status = 401,
description = "Invalid or expired code.",
body = HttpJsonResponse,
),
(
status = 200,
description = "Code valid — JWT issued.",
body = MyceliumLoginResponse,
),
),
security(()),
)]
#[post("/magic-link/verify")]
pub async fn verify_magic_link_url(
body: web::Json<MagicLinkVerifyBody>,
app_module: web::Data<SqlAppModule>,
auth_config: web::Data<InternalOauthConfig>,
core_config: web::Data<AccountLifeCycle>,
) -> impl Responder {
let email = match Email::from_string(body.email.to_owned()) {
Err(err) => {
warn!("Invalid email: {}", err);
return HttpResponse::BadRequest().json(
HttpJsonResponse::new_message(
"Invalid email address.".to_string(),
),
);
}
Ok(email) => email,
};
let user = match verify_magic_link(
email,
body.code.to_owned(),
Box::new(&*app_module.resolve_ref()),
Box::new(&*app_module.resolve_ref()),
Box::new(&*app_module.resolve_ref()),
Box::new(&*app_module.resolve_ref()),
)
.await
{
Ok(user) => user,
Err(err) => return handle_mapped_error(err),
};
match encode_jwt(
user.to_owned(),
auth_config.get_ref().to_owned(),
core_config.get_ref().to_owned(),
false,
)
.await
{
Err(err) => err,
Ok((token, duration)) => {
HttpResponse::Ok().json(MyceliumLoginResponse {
token,
duration,
totp_required: false,
user,
})
}
}
}