use rustlavel::prelude::*;
use crate::models::login_attempt::LoginAttempt;
use crate::models::user::User;
use crate::models::user_token::MAGIC_LINK;
use crate::support::{lockout, page, tokens};
use super::login_controller::{LoginController, PENDING_KEY};
pub struct MagicLinkController;
impl MagicLinkController {
pub async fn show(req: Request) -> Result<Response> {
if !enabled(&req).await {
return Ok(Response::not_found());
}
let context = page::shell(&req, "").await;
req.view("auth/magic", &page::old(context, &[("email", None)]))
}
pub async fn store(mut req: Request) -> Result<Response> {
if !enabled(&req).await {
return Ok(Response::not_found());
}
let db = req.state::<Database>().expect("the database is registered in main.rs").clone();
let email = req.input("email").unwrap_or_default().trim().to_lowercase();
let errors = page::check(&[("email", &email)], &[("email", "required|email|max:190")]);
if !errors.is_empty() {
let context = page::errors(page::shell(&req, "").await, &errors);
return req.view("auth/magic", &page::old(context, &[("email", Some(email))]));
}
if lockout::address_is_blocked(&req).await {
return Self::sent(req, &email).await;
}
lockout::record_address_failure(&req).await;
if let Some(user) = User::first(&db, User::by_email(&email)).await? {
if user.can_sign_in(&tokens::now()).is_ok() {
send_link(&req, &db, &user).await?;
}
}
Self::sent(req, &email).await
}
pub async fn consume(req: Request) -> Result<Response> {
if !enabled(&req).await {
return Ok(Response::not_found());
}
let db = req.state::<Database>().expect("the database is registered in main.rs").clone();
let token = req.param("token").unwrap_or_default().to_string();
let now = tokens::now();
let Some(record) = tokens::claim(&db, MAGIC_LINK, &token).await? else {
return super::register_controller::expired(
req,
"That sign-in link has expired or has already been used.",
"/magic-link",
"Ask for a new link",
)
.await;
};
let Some(mut user) = User::find(&db, record.user_id).await? else {
return super::register_controller::expired(req, "That account no longer exists.", "/login", "Back to sign in").await;
};
if let Err(reason) = user.can_sign_in(&now) {
LoginAttempt::record(&db, &user.email.clone(), Some(user.id), false, Some(reason), &req).await?;
return super::register_controller::expired(
req,
"That account cannot sign in at the moment.",
"/login",
"Back to sign in",
)
.await;
}
if super::mfa_controller::has_factor(&db, user.id).await? {
let session = req.session();
session.regenerate();
session.put(PENDING_KEY, Json::from(user.id));
return Ok(Response::see_other("/mfa"));
}
LoginController::complete(&req, &db, &mut user, &now).await?;
if let Some(enrol) = LoginController::enrolment_owed(&req, &db, user.id).await? {
return Ok(Response::see_other(enrol));
}
Ok(Response::see_other("/dashboard"))
}
async fn sent(req: Request, email: &str) -> Result<Response> {
let context = page::shell(&req, "").await
.with("email", Json::from(email))
.with("expires_in", Json::from("one hour"));
req.view("auth/sent", &context)
}
}
async fn send_link(req: &Request, db: &Database, user: &User) -> Result<()> {
let token = tokens::issue(db, user.id, MAGIC_LINK, None).await?;
let url = format!("{}/magic/{token}", req.config().string("app.url", "http://localhost:8000"));
if req.state::<rustlavel::mail::Mailer>().is_none() {
warn!("no mailer is configured; the sign-in link for {} is {url}", user.email);
return Ok(());
}
crate::support::mail::send(
req,
rustlavel::mail::Message::new()
.to(user.email.as_str())
.subject("Your sign-in link")
.text(format!(
"Hello {},\n\nUse this link to sign in:\n\n{url}\n\nThe link works once and \
expires in an hour. If you did not ask for it, you can ignore it — nobody \
can sign in as you without it.\n",
user.first_name()
)),
)
.await
}
pub async fn enabled(req: &Request) -> bool {
match req.state::<crate::support::settings::Settings>() {
Some(settings) => settings.bool("auth.magic_link").await,
None => req.config().bool("auth.magic_link", false),
}
}