Make the password registration create a user_registration

This commit is contained in:
Quentin Gliech
2025-01-14 12:57:01 +01:00
parent d9a34327f9
commit 90fb2f0369
6 changed files with 118 additions and 57 deletions

View File

@@ -24,8 +24,8 @@ use mas_matrix::BoxHomeserverConnection;
use mas_policy::Policy;
use mas_router::UrlBuilder;
use mas_storage::{
queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
user::{BrowserSessionRepository, UserEmailRepository, UserPasswordRepository, UserRepository},
queue::{QueueJobRepositoryExt as _, SendEmailAuthenticationCodeJob},
user::{UserEmailRepository, UserRepository},
BoxClock, BoxRepository, BoxRng, RepositoryAccess,
};
use mas_templates::{
@@ -141,6 +141,8 @@ pub(crate) async fn post(
Form(form): Form<ProtectedForm<RegisterForm>>,
) -> Result<Response, FancyError> {
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
let ip_address = activity_tracker.ip();
if !site_config.password_registration_enabled {
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
}
@@ -296,49 +298,64 @@ pub(crate) async fn post(
return Ok((cookie_jar, Html(content)).into_response());
}
let user = repo.user().add(&mut rng, &clock, form.username).await?;
let post_auth_action = query
.post_auth_action
.map(serde_json::to_value)
.transpose()?;
let registration = repo
.user_registration()
.add(
&mut rng,
&clock,
form.username,
ip_address,
user_agent,
post_auth_action,
)
.await?;
if let Some(tos_uri) = &site_config.tos_uri {
repo.user_terms()
.accept_terms(&mut rng, &clock, &user, tos_uri.clone())
.await?;
}
let registration = if let Some(tos_uri) = &site_config.tos_uri {
repo.user_registration()
.set_terms_url(registration, tos_uri.clone())
.await?
} else {
registration
};
// Create a new user email authentication session
let user_email_authentication = repo
.user_email()
.add_authentication_for_registration(&mut rng, &clock, form.email, &registration)
.await?;
// Schedule a job to verify the email
repo.queue_job()
.schedule_job(
&mut rng,
&clock,
SendEmailAuthenticationCodeJob::new(&user_email_authentication, locale.to_string()),
)
.await?;
let registration = repo
.user_registration()
.set_email_authentication(registration, &user_email_authentication)
.await?;
// Hash the password
let password = Zeroizing::new(form.password.into_bytes());
let (version, hashed_password) = password_manager.hash(&mut rng, password).await?;
let user_password = repo
.user_password()
.add(&mut rng, &clock, &user, version, hashed_password, None)
.await?;
let user_email = repo
.user_email()
.add(&mut rng, &clock, &user, form.email)
.await?;
let next = mas_router::AccountVerifyEmail::new(user_email.id).and_maybe(query.post_auth_action);
let session = repo
.browser_session()
.add(&mut rng, &clock, &user, user_agent)
.await?;
repo.browser_session()
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
.await?;
repo.queue_job()
.schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user))
// Add the password to the registration
let registration = repo
.user_registration()
.set_password(registration, hashed_password, version)
.await?;
repo.save().await?;
activity_tracker
.record_browser_session(&clock, &session)
.await;
let cookie_jar = cookie_jar.set_session(&session);
Ok((cookie_jar, url_builder.redirect(&next)).into_response())
// TODO: redirect to the next step on the registration
Ok(format!("{}", registration.id).into_response())
}
async fn render(
@@ -451,16 +468,23 @@ mod tests {
let request = cookies.with_cookies(request);
let response = state.request(request).await;
cookies.save_cookies(&response);
response.assert_status(StatusCode::SEE_OTHER);
// Now if we get to the home page, we should see the user's username
let request = Request::get("/").empty();
let request = cookies.with_cookies(request);
let response = state.request(request).await;
cookies.save_cookies(&response);
response.assert_status(StatusCode::OK);
response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8");
assert!(response.body().contains("john"));
// The handler gives us the registration ID in the body for now
let id = response.body().parse().unwrap();
// There should be a new registration in the database
let mut repo = state.repository().await.unwrap();
let registration = repo.user_registration().lookup(id).await.unwrap().unwrap();
assert_eq!(registration.username, "john".to_owned());
assert!(registration.password.is_some());
let email_authentication = repo
.user_email()
.lookup_authentication(registration.email_authentication_id.unwrap())
.await
.unwrap()
.unwrap();
assert_eq!(email_authentication.email, "john@example.com");
}
/// When the two password fields mismatch, it should give an error

View File

@@ -78,6 +78,22 @@ impl RunnableJob for SendEmailAuthenticationCodeJob {
None
};
// Load the registration, if any
let registration =
if let Some(registration_id) = user_email_authentication.user_registration_id {
Some(
repo.user_registration()
.lookup(registration_id)
.await
.map_err(JobError::retry)?
.ok_or(JobError::fail(anyhow::anyhow!(
"Failed to load user registration"
)))?,
)
} else {
None
};
// Generate a new 6-digit authentication code
let range = Uniform::<u32>::from(0..1_000_000);
let code = rng.sample(range);
@@ -98,14 +114,17 @@ impl RunnableJob for SendEmailAuthenticationCodeJob {
.email
.parse()
.map_err(JobError::fail)?;
let username = browser_session.as_ref().map(|s| s.user.username.clone());
let username_from_session = browser_session.as_ref().map(|s| s.user.username.clone());
let username_from_registration = registration.as_ref().map(|r| r.username.clone());
let username = username_from_registration.or(username_from_session);
let mailbox = Mailbox::new(username, address);
info!("Sending email verification code to {}", mailbox);
let language = self.language().parse().map_err(JobError::fail)?;
let context = EmailVerificationContext::new(code, browser_session).with_language(language);
let context = EmailVerificationContext::new(code, browser_session, registration)
.with_language(language);
mailer
.send_verification_email(mailbox, &context)
.await

View File

@@ -23,7 +23,7 @@ use mas_data_model::{
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode,
UpstreamOAuthProviderTokenAuthMethod, User, UserAgent, UserEmail, UserEmailAuthenticationCode,
UserRecoverySession,
UserRecoverySession, UserRegistration,
};
use mas_i18n::DataLocale;
use mas_iana::jose::JsonWebSignatureAlg;
@@ -878,7 +878,10 @@ impl TemplateContext for EmailRecoveryContext {
/// Context used by the `emails/verification.{txt,html,subject}` templates
#[derive(Serialize)]
pub struct EmailVerificationContext {
#[serde(skip_serializing_if = "Option::is_none")]
browser_session: Option<BrowserSession>,
#[serde(skip_serializing_if = "Option::is_none")]
user_registration: Option<UserRegistration>,
authentication_code: UserEmailAuthenticationCode,
}
@@ -888,9 +891,11 @@ impl EmailVerificationContext {
pub fn new(
authentication_code: UserEmailAuthenticationCode,
browser_session: Option<BrowserSession>,
user_registration: Option<UserRegistration>,
) -> Self {
Self {
browser_session,
user_registration,
authentication_code,
}
}
@@ -926,6 +931,7 @@ impl TemplateContext for EmailVerificationContext {
Self {
browser_session: Some(browser_session),
user_registration: None,
authentication_code,
}
})

View File

@@ -8,6 +8,12 @@ Please see LICENSE in the repository root for full details.
{%- set _ = translator(lang) -%}
{{ _("mas.emails.greeting", username=browser_session.user.username | default("user")) }}<br />
{%- if browser_session is defined -%}
{%- set username = browser_session.user.username -%}
{%- elif user_registration is defined -%}
{%- set username = user_registration.username -%}
{%- endif -%}
{{ _("mas.emails.greeting", username=(username|default("user"))) }}<br />
<br />
{{ _("mas.emails.verify.body_html", code=authentication_code.code) }}<br />

View File

@@ -8,6 +8,12 @@ Please see LICENSE in the repository root for full details.
{%- set _ = translator(lang) -%}
{{ _("mas.emails.greeting", username=browser_session.user.username | default("user")) }}
{%- if browser_session is defined -%}
{%- set username = browser_session.user.username -%}
{%- elif user_registration is defined -%}
{%- set username = user_registration.username -%}
{%- endif -%}
{{ _("mas.emails.greeting", username=(username|default("user"))) }}
{{ _("mas.emails.verify.body_text", code=authentication_code.code) }}

View File

@@ -219,7 +219,7 @@
"emails": {
"greeting": "Hello %(username)s,",
"@greeting": {
"context": "emails/verification.html:11:3-85, emails/verification.txt:11:3-85",
"context": "emails/verification.html:17:3-64, emails/verification.txt:17:3-64",
"description": "Greeting at the top of emails sent to the user"
},
"recovery": {
@@ -251,12 +251,12 @@
"verify": {
"body_html": "Your verification code to confirm this email address is: <strong>%(code)s</strong>",
"@body_html": {
"context": "emails/verification.html:13:3-66",
"context": "emails/verification.html:19:3-66",
"description": "The body of the email sent to verify an email address (HTML)"
},
"body_text": "Your verification code to confirm this email address is: %(code)s",
"@body_text": {
"context": "emails/verification.txt:13:3-66",
"context": "emails/verification.txt:19:3-66",
"description": "The body of the email sent to verify an email address (text)"
},
"subject": "Your email verification code is: %(code)s",
@@ -327,7 +327,7 @@
},
"continue_with_provider": "Continue with %(provider)s",
"@continue_with_provider": {
"context": "pages/login.html:75:15-67, pages/register/index.html:49:15-67",
"context": "pages/login.html:75:15-67, pages/register/index.html:53:15-67",
"description": "Button to log in with an upstream provider"
},
"description": "Please sign in to continue:",
@@ -513,12 +513,12 @@
"register": {
"call_to_login": "Already have an account?",
"@call_to_login": {
"context": "pages/register/index.html:55:35-66, pages/register/password.html:77:33-64",
"context": "pages/register/index.html:59:35-66, pages/register/password.html:77:33-64",
"description": "Displayed on the registration page to suggest to log in instead"
},
"continue_with_email": "Continue with email address",
"@continue_with_email": {
"context": "pages/register/index.html:40:30-67"
"context": "pages/register/index.html:44:30-67"
},
"create_account": {
"description": "Choose a username to continue.",
@@ -682,4 +682,4 @@
}
}
}
}
}