Implement email verification in the registration flow
This commit is contained in:
@@ -379,9 +379,9 @@ where
|
||||
get(self::views::register::password::get).post(self::views::register::password::post),
|
||||
)
|
||||
.route(
|
||||
mas_router::AccountVerifyEmail::route(),
|
||||
get(self::views::account::emails::verify::get)
|
||||
.post(self::views::account::emails::verify::post),
|
||||
mas_router::RegisterVerifyEmail::route(),
|
||||
get(self::views::register::steps::verify_email::get)
|
||||
.post(self::views::register::steps::verify_email::post),
|
||||
)
|
||||
.route(
|
||||
mas_router::AccountRecoveryStart::route(),
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
pub mod verify;
|
||||
@@ -1,135 +0,0 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Form, Path, Query, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use mas_axum_utils::{
|
||||
cookies::CookieJar,
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::{
|
||||
queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
|
||||
user::UserEmailRepository,
|
||||
BoxClock, BoxRepository, BoxRng, RepositoryAccess,
|
||||
};
|
||||
use mas_templates::{EmailVerificationPageContext, TemplateContext, Templates};
|
||||
use serde::Deserialize;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CodeForm {
|
||||
code: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "handlers.views.account_email_verify.get",
|
||||
fields(user_email.id = %id),
|
||||
skip_all,
|
||||
err,
|
||||
)]
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
mut repo: BoxRepository,
|
||||
Query(_query): Query<OptionalPostAuthAction>,
|
||||
Path(id): Path<Ulid>,
|
||||
cookie_jar: CookieJar,
|
||||
) -> Result<Response, FancyError> {
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
|
||||
let maybe_session = session_info.load_session(&mut repo).await?;
|
||||
|
||||
let Some(session) = maybe_session else {
|
||||
let login = mas_router::Login::default();
|
||||
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
|
||||
};
|
||||
|
||||
activity_tracker
|
||||
.record_browser_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
let user_email = repo
|
||||
.user_email()
|
||||
.lookup(id)
|
||||
.await?
|
||||
.filter(|u| u.user_id == session.user.id)
|
||||
.context("Could not find user email")?;
|
||||
|
||||
let ctx = EmailVerificationPageContext::new(user_email)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_account_verify_email(&ctx)?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "handlers.views.account_email_verify.post",
|
||||
fields(user_email.id = %id),
|
||||
skip_all,
|
||||
err,
|
||||
)]
|
||||
pub(crate) async fn post(
|
||||
clock: BoxClock,
|
||||
mut rng: BoxRng,
|
||||
mut repo: BoxRepository,
|
||||
cookie_jar: CookieJar,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
Query(query): Query<OptionalPostAuthAction>,
|
||||
Path(id): Path<Ulid>,
|
||||
Form(form): Form<ProtectedForm<CodeForm>>,
|
||||
) -> Result<Response, FancyError> {
|
||||
let _form = cookie_jar.verify_form(&clock, form)?;
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
|
||||
let maybe_session = session_info.load_session(&mut repo).await?;
|
||||
|
||||
let Some(session) = maybe_session else {
|
||||
let login = mas_router::Login::default();
|
||||
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
|
||||
};
|
||||
|
||||
let _user_email = repo
|
||||
.user_email()
|
||||
.lookup(id)
|
||||
.await?
|
||||
.filter(|u| u.user_id == session.user.id)
|
||||
.context("Could not find user email")?;
|
||||
|
||||
// XXX: this logic should be extracted somewhere else, since most of it is
|
||||
// duplicated in mas_graphql
|
||||
|
||||
// TODO: Use the new email authentication codes
|
||||
|
||||
repo.queue_job()
|
||||
.schedule_job(&mut rng, &clock, ProvisionUserJob::new(&session.user))
|
||||
.await?;
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
activity_tracker
|
||||
.record_browser_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
let destination = query.go_next_or_default(&url_builder, &mas_router::Account::default());
|
||||
Ok((cookie_jar, destination).into_response())
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
pub mod account;
|
||||
pub mod app;
|
||||
pub mod index;
|
||||
pub mod login;
|
||||
|
||||
@@ -18,6 +18,7 @@ use super::shared::OptionalPostAuthAction;
|
||||
use crate::{BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
pub(crate) mod password;
|
||||
pub(crate) mod steps;
|
||||
|
||||
#[tracing::instrument(name = "handlers.views.register.get", skip_all, err)]
|
||||
pub(crate) async fn get(
|
||||
|
||||
@@ -354,8 +354,9 @@ pub(crate) async fn post(
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
// TODO: redirect to the next step on the registration
|
||||
Ok(format!("{}", registration.id).into_response())
|
||||
Ok(url_builder
|
||||
.redirect(&mas_router::RegisterVerifyEmail::new(registration.id))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn render(
|
||||
@@ -468,10 +469,19 @@ mod tests {
|
||||
let request = cookies.with_cookies(request);
|
||||
let response = state.request(request).await;
|
||||
cookies.save_cookies(&response);
|
||||
response.assert_status(StatusCode::OK);
|
||||
response.assert_status(StatusCode::SEE_OTHER);
|
||||
let location = response.headers().get(LOCATION).unwrap();
|
||||
|
||||
// The handler redirects with the ID as the last portion of the path
|
||||
let id = location
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
pub mod emails;
|
||||
pub(crate) mod verify_email;
|
||||
253
crates/handlers/src/views/register/steps/verify_email.rs
Normal file
253
crates/handlers/src/views/register/steps/verify_email.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Form, Path, State},
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::TypedHeader;
|
||||
use mas_axum_utils::{
|
||||
cookies::CookieJar,
|
||||
csrf::{CsrfExt, ProtectedForm},
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_data_model::UserAgent;
|
||||
use mas_router::{PostAuthAction, UrlBuilder};
|
||||
use mas_storage::{
|
||||
queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
|
||||
user::UserEmailRepository,
|
||||
BoxClock, BoxRepository, BoxRng, RepositoryAccess,
|
||||
};
|
||||
use mas_templates::{
|
||||
FieldError, RegisterStepsVerifyEmailContext, RegisterStepsVerifyEmailFormField,
|
||||
TemplateContext, Templates, ToFormState,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CodeForm {
|
||||
code: String,
|
||||
}
|
||||
|
||||
impl ToFormState for CodeForm {
|
||||
type Field = mas_templates::RegisterStepsVerifyEmailFormField;
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "handlers.views.register.steps.verify_email.get",
|
||||
fields(user_registration.id = %id),
|
||||
skip_all,
|
||||
err,
|
||||
)]
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
mut repo: BoxRepository,
|
||||
Path(id): Path<Ulid>,
|
||||
cookie_jar: CookieJar,
|
||||
) -> Result<Response, FancyError> {
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.lookup(id)
|
||||
.await?
|
||||
.context("Could not find user registration")?;
|
||||
|
||||
// If the registration is completed, we can go to the registration destination
|
||||
// XXX: this might not be the right thing to do? Maybe an error page would be
|
||||
// better?
|
||||
if registration.completed_at.is_some() {
|
||||
let post_auth_action: Option<PostAuthAction> = registration
|
||||
.post_auth_action
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?;
|
||||
|
||||
return Ok(OptionalPostAuthAction::from(post_auth_action)
|
||||
.go_next(&url_builder)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let email_authentication_id = registration
|
||||
.email_authentication_id
|
||||
.context("No email authentication started for this registration")?;
|
||||
let email_authentication = repo
|
||||
.user_email()
|
||||
.lookup_authentication(email_authentication_id)
|
||||
.await?
|
||||
.context("Could not find email authentication")?;
|
||||
|
||||
if email_authentication.completed_at.is_some() {
|
||||
// XXX: display a better error here
|
||||
return Err(FancyError::from(anyhow::anyhow!(
|
||||
"Email authentication already completed"
|
||||
)));
|
||||
}
|
||||
|
||||
let ctx = RegisterStepsVerifyEmailContext::new(email_authentication)
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_register_steps_verify_email(&ctx)?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "handlers.views.account_email_verify.post",
|
||||
fields(user_email.id = %id),
|
||||
skip_all,
|
||||
err,
|
||||
)]
|
||||
pub(crate) async fn post(
|
||||
clock: BoxClock,
|
||||
mut rng: BoxRng,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
mut repo: BoxRepository,
|
||||
cookie_jar: CookieJar,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
Path(id): Path<Ulid>,
|
||||
Form(form): Form<ProtectedForm<CodeForm>>,
|
||||
) -> Result<Response, FancyError> {
|
||||
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
|
||||
let form = cookie_jar.verify_form(&clock, form)?;
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.lookup(id)
|
||||
.await?
|
||||
.context("Could not find user registration")?;
|
||||
|
||||
// If the registration is completed, we can go to the registration destination
|
||||
// XXX: this might not be the right thing to do? Maybe an error page would be
|
||||
// better?
|
||||
if registration.completed_at.is_some() {
|
||||
let post_auth_action: Option<PostAuthAction> = registration
|
||||
.post_auth_action
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?;
|
||||
|
||||
return Ok(OptionalPostAuthAction::from(post_auth_action)
|
||||
.go_next(&url_builder)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let email_authentication_id = registration
|
||||
.email_authentication_id
|
||||
.context("No email authentication started for this registration")?;
|
||||
let email_authentication = repo
|
||||
.user_email()
|
||||
.lookup_authentication(email_authentication_id)
|
||||
.await?
|
||||
.context("Could not find email authentication")?;
|
||||
|
||||
if email_authentication.completed_at.is_some() {
|
||||
// XXX: display a better error here
|
||||
return Err(FancyError::from(anyhow::anyhow!(
|
||||
"Email authentication already completed"
|
||||
)));
|
||||
}
|
||||
|
||||
let Some(code) = repo
|
||||
.user_email()
|
||||
.find_authentication_code(&email_authentication, &form.code)
|
||||
.await?
|
||||
else {
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
let ctx =
|
||||
RegisterStepsVerifyEmailContext::new(email_authentication)
|
||||
.with_form_state(form.to_form_state().with_error_on_field(
|
||||
RegisterStepsVerifyEmailFormField::Code,
|
||||
FieldError::Invalid,
|
||||
))
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_register_steps_verify_email(&ctx)?;
|
||||
|
||||
return Ok((cookie_jar, Html(content)).into_response());
|
||||
};
|
||||
|
||||
let email_authentication = repo
|
||||
.user_email()
|
||||
.complete_authentication(&clock, email_authentication, &code)
|
||||
.await?;
|
||||
|
||||
let registration = repo
|
||||
.user_registration()
|
||||
.complete(&clock, registration)
|
||||
.await?;
|
||||
|
||||
// XXX: this should move somewhere else, and it doesn't check for uniqueness
|
||||
let user = repo
|
||||
.user()
|
||||
.add(&mut rng, &clock, registration.username)
|
||||
.await?;
|
||||
let user_session = repo
|
||||
.browser_session()
|
||||
.add(&mut rng, &clock, &user, user_agent)
|
||||
.await?;
|
||||
|
||||
repo.user_email()
|
||||
.add(&mut rng, &clock, &user, email_authentication.email)
|
||||
.await?;
|
||||
|
||||
if let Some(password) = registration.password {
|
||||
let user_password = repo
|
||||
.user_password()
|
||||
.add(
|
||||
&mut rng,
|
||||
&clock,
|
||||
&user,
|
||||
password.version,
|
||||
password.hashed_password,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
repo.browser_session()
|
||||
.authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(terms_url) = registration.terms_url {
|
||||
repo.user_terms()
|
||||
.accept_terms(&mut rng, &clock, &user, terms_url)
|
||||
.await?;
|
||||
}
|
||||
|
||||
repo.queue_job()
|
||||
.schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user))
|
||||
.await?;
|
||||
|
||||
repo.save().await?;
|
||||
|
||||
activity_tracker
|
||||
.record_browser_session(&clock, &user_session)
|
||||
.await;
|
||||
|
||||
let post_auth_action: Option<PostAuthAction> = registration
|
||||
.post_auth_action
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?;
|
||||
|
||||
let cookie_jar = cookie_jar.set_session(&user_session);
|
||||
|
||||
return Ok((
|
||||
cookie_jar,
|
||||
OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
@@ -444,47 +444,27 @@ impl From<Option<PostAuthAction>> for PasswordRegister {
|
||||
}
|
||||
}
|
||||
|
||||
/// `GET|POST /verify-email/:id`
|
||||
/// `GET|POST /register/steps/verify-email/:id`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccountVerifyEmail {
|
||||
pub struct RegisterVerifyEmail {
|
||||
id: Ulid,
|
||||
post_auth_action: Option<PostAuthAction>,
|
||||
}
|
||||
|
||||
impl AccountVerifyEmail {
|
||||
impl RegisterVerifyEmail {
|
||||
#[must_use]
|
||||
pub fn new(id: Ulid) -> Self {
|
||||
Self {
|
||||
id,
|
||||
post_auth_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn and_maybe(mut self, action: Option<PostAuthAction>) -> Self {
|
||||
self.post_auth_action = action;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn and_then(mut self, action: PostAuthAction) -> Self {
|
||||
self.post_auth_action = Some(action);
|
||||
self
|
||||
Self { id }
|
||||
}
|
||||
}
|
||||
|
||||
impl Route for AccountVerifyEmail {
|
||||
type Query = PostAuthAction;
|
||||
impl Route for RegisterVerifyEmail {
|
||||
type Query = ();
|
||||
fn route() -> &'static str {
|
||||
"/verify-email/:id"
|
||||
}
|
||||
|
||||
fn query(&self) -> Option<&Self::Query> {
|
||||
self.post_auth_action.as_ref()
|
||||
"/register/steps/verify-email/:id"
|
||||
}
|
||||
|
||||
fn path(&self) -> std::borrow::Cow<'static, str> {
|
||||
format!("/verify-email/{}", self.id).into()
|
||||
format!("/register/steps/verify-email/{}", self.id).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ use mas_data_model::{
|
||||
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
|
||||
DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
|
||||
UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode,
|
||||
UpstreamOAuthProviderTokenAuthMethod, User, UserAgent, UserEmail, UserEmailAuthenticationCode,
|
||||
UserRecoverySession, UserRegistration,
|
||||
UpstreamOAuthProviderTokenAuthMethod, User, UserAgent, UserEmailAuthentication,
|
||||
UserEmailAuthenticationCode, UserRecoverySession, UserRegistration,
|
||||
};
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
@@ -942,12 +942,12 @@ impl TemplateContext for EmailVerificationContext {
|
||||
/// Fields of the email verification form
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EmailVerificationFormField {
|
||||
pub enum RegisterStepsVerifyEmailFormField {
|
||||
/// The code field
|
||||
Code,
|
||||
}
|
||||
|
||||
impl FormField for EmailVerificationFormField {
|
||||
impl FormField for RegisterStepsVerifyEmailFormField {
|
||||
fn keep(&self) -> bool {
|
||||
match self {
|
||||
Self::Code => true,
|
||||
@@ -955,45 +955,47 @@ impl FormField for EmailVerificationFormField {
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `pages/account/verify.html` templates
|
||||
/// Context used by the `pages/register/steps/verify_email.html` templates
|
||||
#[derive(Serialize)]
|
||||
pub struct EmailVerificationPageContext {
|
||||
form: FormState<EmailVerificationFormField>,
|
||||
email: UserEmail,
|
||||
pub struct RegisterStepsVerifyEmailContext {
|
||||
form: FormState<RegisterStepsVerifyEmailFormField>,
|
||||
authentication: UserEmailAuthentication,
|
||||
}
|
||||
|
||||
impl EmailVerificationPageContext {
|
||||
impl RegisterStepsVerifyEmailContext {
|
||||
/// Constructs a context for the email verification page
|
||||
#[must_use]
|
||||
pub fn new(email: UserEmail) -> Self {
|
||||
pub fn new(authentication: UserEmailAuthentication) -> Self {
|
||||
Self {
|
||||
form: FormState::default(),
|
||||
email,
|
||||
authentication,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the form state
|
||||
#[must_use]
|
||||
pub fn with_form_state(self, form: FormState<EmailVerificationFormField>) -> Self {
|
||||
pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
|
||||
Self { form, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateContext for EmailVerificationPageContext {
|
||||
impl TemplateContext for RegisterStepsVerifyEmailContext {
|
||||
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let email = UserEmail {
|
||||
let authentication = UserEmailAuthentication {
|
||||
id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
user_id: Ulid::from_datetime_with_source(now.into(), rng),
|
||||
user_session_id: None,
|
||||
user_registration_id: None,
|
||||
email: "foobar@example.com".to_owned(),
|
||||
created_at: now,
|
||||
completed_at: None,
|
||||
};
|
||||
|
||||
vec![Self {
|
||||
form: FormState::default(),
|
||||
email,
|
||||
authentication,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,14 +36,15 @@ pub use self::{
|
||||
context::{
|
||||
ApiDocContext, AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext,
|
||||
DeviceLinkContext, DeviceLinkFormField, EmailRecoveryContext, EmailVerificationContext,
|
||||
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
||||
LoginContext, LoginFormField, NotFoundContext, PasswordRegisterContext,
|
||||
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
|
||||
ReauthFormField, RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
|
||||
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
|
||||
RegisterFormField, SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext,
|
||||
UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
|
||||
UpstreamSuggestLink, WithCaptcha, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
|
||||
EmptyContext, ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField,
|
||||
NotFoundContext, PasswordRegisterContext, PolicyViolationContext, PostAuthContext,
|
||||
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryExpiredContext,
|
||||
RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext,
|
||||
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
|
||||
RegisterStepsVerifyEmailContext, RegisterStepsVerifyEmailFormField, SiteBranding,
|
||||
SiteConfigExt, SiteFeatures, TemplateContext, UpstreamExistingLinkContext,
|
||||
UpstreamRegister, UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf,
|
||||
WithLanguage, WithOptionalSession, WithSession,
|
||||
},
|
||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||
};
|
||||
@@ -331,6 +332,9 @@ register_templates! {
|
||||
/// Render the password registration page
|
||||
pub fn render_password_register(WithLanguage<WithCsrf<WithCaptcha<PasswordRegisterContext>>>) { "pages/register/password.html" }
|
||||
|
||||
/// Render the email verification page
|
||||
pub fn render_register_steps_verify_email(WithLanguage<WithCsrf<RegisterStepsVerifyEmailContext>>) { "pages/register/steps/verify_email.html" }
|
||||
|
||||
/// Render the client consent page
|
||||
pub fn render_consent(WithLanguage<WithCsrf<WithSession<ConsentContext>>>) { "pages/consent.html" }
|
||||
|
||||
@@ -343,9 +347,6 @@ register_templates! {
|
||||
/// Render the home page
|
||||
pub fn render_index(WithLanguage<WithCsrf<WithOptionalSession<IndexContext>>>) { "pages/index.html" }
|
||||
|
||||
/// Render the email verification page
|
||||
pub fn render_account_verify_email(WithLanguage<WithCsrf<WithSession<EmailVerificationPageContext>>>) { "pages/account/emails/verify.html" }
|
||||
|
||||
/// Render the account recovery start page
|
||||
pub fn render_recovery_start(WithLanguage<WithCsrf<RecoveryStartContext>>) { "pages/recovery/start.html" }
|
||||
|
||||
@@ -425,11 +426,12 @@ impl Templates {
|
||||
check::render_swagger_callback(self, now, rng)?;
|
||||
check::render_login(self, now, rng)?;
|
||||
check::render_register(self, now, rng)?;
|
||||
check::render_password_register(self, now, rng)?;
|
||||
check::render_register_steps_verify_email(self, now, rng)?;
|
||||
check::render_consent(self, now, rng)?;
|
||||
check::render_policy_violation(self, now, rng)?;
|
||||
check::render_sso_login(self, now, rng)?;
|
||||
check::render_index(self, now, rng)?;
|
||||
check::render_account_verify_email(self, now, rng)?;
|
||||
check::render_recovery_start(self, now, rng)?;
|
||||
check::render_recovery_progress(self, now, rng)?;
|
||||
check::render_recovery_finish(self, now, rng)?;
|
||||
|
||||
@@ -15,7 +15,7 @@ Please see LICENSE in the repository root for full details.
|
||||
</div>
|
||||
<div class="header">
|
||||
<h1 class="title">{{ _("mas.verify_email.headline") }}</h1>
|
||||
<p class="text">{{ _("mas.verify_email.description", email=email.email) }}</p>
|
||||
<p class="text">{{ _("mas.verify_email.description", email=authentication.email) }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -30,7 +30,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
|
||||
{% call(f) field.field(label=_("mas.verify_email.6_digit_code"), name="code", class="mb-4 self-center") %}
|
||||
{% call(f) field.field(label=_("mas.verify_email.6_digit_code"), name="code", form_state=form, class="mb-4 self-center") %}
|
||||
<div class="cpd-mfa-container">
|
||||
<input {{ field.attributes(f) }}
|
||||
id="mfa-code-input"
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"continue": "Continue",
|
||||
"@continue": {
|
||||
"context": "form_post.html:25:28-48, pages/account/emails/verify.html:52:26-46, pages/consent.html:57:28-48, pages/device_consent.html:123:13-33, pages/device_link.html:40:26-46, pages/login.html:62:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/sso.html:37:28-48"
|
||||
"context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:123:13-33, pages/device_link.html:40:26-46, pages/login.html:62:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/verify_email.html:52:26-46, pages/sso.html:37:28-48"
|
||||
},
|
||||
"create_account": "Create Account",
|
||||
"@create_account": {
|
||||
@@ -670,15 +670,15 @@
|
||||
"verify_email": {
|
||||
"6_digit_code": "6-digit code",
|
||||
"@6_digit_code": {
|
||||
"context": "pages/account/emails/verify.html:33:33-67"
|
||||
"context": "pages/register/steps/verify_email.html:33:33-67"
|
||||
},
|
||||
"description": "Enter the 6-digit code sent to: <em>%(email)s</em>",
|
||||
"@description": {
|
||||
"context": "pages/account/emails/verify.html:18:25-77"
|
||||
"context": "pages/register/steps/verify_email.html:18:25-86"
|
||||
},
|
||||
"headline": "Verify your email",
|
||||
"@headline": {
|
||||
"context": "pages/account/emails/verify.html:17:27-57"
|
||||
"context": "pages/register/steps/verify_email.html:17:27-57"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user