1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

Switch email verification to a code-based flow

This commit is contained in:
Quentin Gliech
2022-05-24 12:31:05 +02:00
parent 35fa7c732a
commit 89597dbf81
18 changed files with 409 additions and 236 deletions

View File

@ -173,6 +173,7 @@ pub enum UserEmailVerificationState {
pub struct UserEmailVerification<T: StorageBackend> {
pub data: T::UserEmailVerificationData,
pub email: UserEmail<T>,
pub code: String,
pub created_at: DateTime<Utc>,
pub state: UserEmailVerificationState,
}
@ -182,6 +183,7 @@ impl<S: StorageBackendMarker> From<UserEmailVerification<S>> for UserEmailVerifi
Self {
data: (),
email: v.email.into(),
code: v.code,
created_at: v.created_at,
state: v.state,
}
@ -207,6 +209,7 @@ where
.flat_map(|state| {
UserEmail::samples().into_iter().map(move |email| Self {
data: Default::default(),
code: "123456".to_string(),
email,
created_at: Utc::now() - Duration::minutes(10),
state: state.clone(),

View File

@ -71,10 +71,14 @@ impl Mailer {
let multipart = MultiPart::alternative_plain_html(plain, html);
let subject = self
.templates
.render_email_verification_subject(context)
.await?;
let message = self
.base_message()
// TODO: template/localize this
.subject("Verify your email address")
.subject(subject.trim())
.to(to)
.multipart(multipart)?;

View File

@ -159,8 +159,9 @@ where
get(self::views::register::get).post(self::views::register::post),
)
.route(
mas_router::VerifyEmail::route(),
get(self::views::verify::get),
mas_router::AccountVerifyEmail::route(),
get(self::views::account::emails::verify::get)
.post(self::views::account::emails::verify::post),
)
.route(mas_router::Account::route(), get(self::views::account::get))
.route(

View File

@ -25,7 +25,7 @@ use mas_axum_utils::{
use mas_config::Encrypter;
use mas_data_model::{BrowserSession, User, UserEmail};
use mas_email::Mailer;
use mas_router::{Route, UrlBuilder};
use mas_router::Route;
use mas_storage::{
user::{
add_user_email, add_user_email_verification_code, get_user_email, get_user_emails,
@ -34,16 +34,17 @@ use mas_storage::{
PostgresqlBackend,
};
use mas_templates::{AccountEmailsContext, EmailVerificationContext, TemplateContext, Templates};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rand::{distributions::Uniform, thread_rng, Rng};
use serde::Deserialize;
use sqlx::{PgExecutor, PgPool};
use tracing::info;
pub mod verify;
#[derive(Deserialize, Debug)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum ManagementForm {
Add { email: String },
ResendConfirmation { data: String },
SetPrimary { data: String },
Remove { data: String },
}
@ -88,39 +89,35 @@ async fn render(
async fn start_email_verification(
mailer: &Mailer,
url_builder: &UrlBuilder,
executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>,
user_email: &UserEmail<PostgresqlBackend>,
user_email: UserEmail<PostgresqlBackend>,
) -> anyhow::Result<()> {
// First, generate a code
let code: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect();
let range = Uniform::<u32>::from(0..1_000_000);
let code = thread_rng().sample(range).to_string();
add_user_email_verification_code(executor, user_email, &code).await?;
// And send the verification email
let address: Address = user_email.email.parse()?;
let verification = add_user_email_verification_code(executor, user_email, code).await?;
// And send the verification email
let mailbox = Mailbox::new(Some(user.username.clone()), address);
let link = url_builder.email_verification(code);
let context = EmailVerificationContext::new(user.clone().into(), link);
let context = EmailVerificationContext::new(user.clone().into(), verification.clone().into());
mailer.send_verification_email(mailbox, &context).await?;
info!(email.id = user_email.data, "Verification email sent");
info!(
email.id = verification.email.data,
"Verification email sent"
);
Ok(())
}
pub(crate) async fn post(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
Extension(url_builder): Extension<UrlBuilder>,
Extension(mailer): Extension<Mailer>,
cookie_jar: PrivateCookieJar<Encrypter>,
Form(form): Form<ProtectedForm<ManagementForm>>,
@ -143,8 +140,10 @@ pub(crate) async fn post(
match form {
ManagementForm::Add { email } => {
let user_email = add_user_email(&mut txn, &session.user, email).await?;
start_email_verification(&mailer, &url_builder, &mut txn, &session.user, &user_email)
.await?;
let next = mas_router::AccountVerifyEmail(user_email.data);
start_email_verification(&mailer, &mut txn, &session.user, user_email).await?;
txn.commit().await?;
return Ok((cookie_jar, next.go()).into_response());
}
ManagementForm::Remove { data } => {
let id = data.parse()?;
@ -152,14 +151,6 @@ pub(crate) async fn post(
let email = get_user_email(&mut txn, &session.user, id).await?;
remove_user_email(&mut txn, email).await?;
}
ManagementForm::ResendConfirmation { data } => {
let id = data.parse()?;
let user_email = get_user_email(&mut txn, &session.user, id).await?;
start_email_verification(&mailer, &url_builder, &mut txn, &session.user, &user_email)
.await?;
}
ManagementForm::SetPrimary { data } => {
let id = data.parse()?;
let email = get_user_email(&mut txn, &session.user, id).await?;

View File

@ -0,0 +1,119 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use axum::{
extract::{Extension, Form, Path, Query},
response::{Html, IntoResponse, Response},
};
use axum_extra::extract::PrivateCookieJar;
use chrono::Duration;
use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt,
};
use mas_config::Encrypter;
use mas_router::Route;
use mas_storage::user::{
consume_email_verification, lookup_user_email_by_id, lookup_user_email_verification_code,
mark_user_email_as_verified,
};
use mas_templates::{EmailVerificationPageContext, TemplateContext, Templates};
use serde::Deserialize;
use sqlx::PgPool;
use crate::views::shared::OptionalPostAuthAction;
#[derive(Deserialize, Debug)]
pub struct CodeForm {
code: String,
}
pub(crate) async fn get(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
Query(query): Query<OptionalPostAuthAction>,
Path(id): Path<i64>,
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<Response, FancyError> {
let mut txn = pool.begin().await?;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut txn).await?;
let session = if let Some(session) = maybe_session {
session
} else {
let login = mas_router::Login::default();
return Ok((cookie_jar, login.go()).into_response());
};
let user_email = lookup_user_email_by_id(&mut txn, &session.user, id).await?;
if user_email.confirmed_at.is_some() {
// This email was already verified, skip
let destination = query.go_next_or_default(&mas_router::AccountEmails);
return Ok((cookie_jar, destination).into_response());
}
let ctx = EmailVerificationPageContext::new(user_email)
.with_session(session)
.with_csrf(csrf_token.form_value());
let content = templates.render_email_verification_form(&ctx).await?;
txn.commit().await?;
Ok((cookie_jar, Html(content)).into_response())
}
pub(crate) async fn post(
Extension(pool): Extension<PgPool>,
cookie_jar: PrivateCookieJar<Encrypter>,
Query(query): Query<OptionalPostAuthAction>,
Path(id): Path<i64>,
Form(form): Form<ProtectedForm<CodeForm>>,
) -> Result<Response, FancyError> {
let mut txn = pool.begin().await?;
let form = cookie_jar.verify_form(form)?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut txn).await?;
let session = if let Some(session) = maybe_session {
session
} else {
let login = mas_router::Login::default();
return Ok((cookie_jar, login.go()).into_response());
};
let email = lookup_user_email_by_id(&mut txn, &session.user, id).await?;
// TODO: make those 8 hours configurable
let verification =
lookup_user_email_verification_code(&mut txn, email, &form.code, Duration::hours(8))
.await?;
// TODO: display nice errors if the code was already consumed or expired
let verification = consume_email_verification(&mut txn, verification).await?;
let _email = mark_user_email_as_verified(&mut txn, verification.email).await?;
txn.commit().await?;
let destination = query.go_next_or_default(&mas_router::AccountEmails);
Ok((cookie_jar, destination).into_response())
}

View File

@ -19,4 +19,3 @@ pub mod logout;
pub mod reauth;
pub mod register;
pub mod shared;
pub mod verify;

View File

@ -27,12 +27,16 @@ pub(crate) struct OptionalPostAuthAction {
}
impl OptionalPostAuthAction {
pub fn go_next(&self) -> axum::response::Redirect {
self.post_auth_action.as_ref().map_or_else(
|| mas_router::Index.go(),
mas_router::PostAuthAction::go_next,
)
pub fn go_next_or_default<T: Route>(&self, default: &T) -> axum::response::Redirect {
self.post_auth_action
.as_ref()
.map_or_else(|| default.go(), mas_router::PostAuthAction::go_next)
}
pub fn go_next(&self) -> axum::response::Redirect {
self.go_next_or_default(&mas_router::Index)
}
pub async fn load_context<'e>(
&self,
conn: &mut PgConnection,

View File

@ -1,60 +0,0 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use axum::{
extract::{Extension, Path},
response::{Html, IntoResponse},
};
use axum_extra::extract::PrivateCookieJar;
use chrono::Duration;
use mas_axum_utils::{csrf::CsrfExt, FancyError, SessionInfoExt};
use mas_config::Encrypter;
use mas_storage::user::{
consume_email_verification, lookup_user_email_verification_code, mark_user_email_as_verified,
};
use mas_templates::{EmptyContext, TemplateContext, Templates};
use sqlx::PgPool;
pub(crate) async fn get(
Extension(templates): Extension<Templates>,
Extension(pool): Extension<PgPool>,
Path(code): Path<String>,
cookie_jar: PrivateCookieJar<Encrypter>,
) -> Result<impl IntoResponse, FancyError> {
let mut txn = pool.begin().await?;
// TODO: make those 8 hours configurable
let verification =
lookup_user_email_verification_code(&mut txn, &code, Duration::hours(8)).await?;
// TODO: display nice errors if the code was already consumed or expired
let verification = consume_email_verification(&mut txn, verification).await?;
let _email = mark_user_email_as_verified(&mut txn, verification.email).await?;
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut txn).await?;
let ctx = EmptyContext
.maybe_with_session(maybe_session)
.with_csrf(csrf_token.form_value());
let content = templates.render_email_verification_done(&ctx).await?;
txn.commit().await?;
Ok((cookie_jar, Html(content)))
}

View File

@ -52,7 +52,7 @@ impl PostAuthAction {
}
/// `GET /.well-known/openid-configuration`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct OidcConfiguration;
impl SimpleRoute for OidcConfiguration {
@ -60,7 +60,7 @@ impl SimpleRoute for OidcConfiguration {
}
/// `GET /.well-known/webfinger`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct Webfinger;
impl SimpleRoute for Webfinger {
@ -75,7 +75,7 @@ impl SimpleRoute for ChangePasswordDiscovery {
}
/// `GET /oauth2/keys.json`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct OAuth2Keys;
impl SimpleRoute for OAuth2Keys {
@ -83,7 +83,7 @@ impl SimpleRoute for OAuth2Keys {
}
/// `GET /oauth2/userinfo`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct OidcUserinfo;
impl SimpleRoute for OidcUserinfo {
@ -91,7 +91,7 @@ impl SimpleRoute for OidcUserinfo {
}
/// `POST /oauth2/userinfo`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct OAuth2Introspection;
impl SimpleRoute for OAuth2Introspection {
@ -99,7 +99,7 @@ impl SimpleRoute for OAuth2Introspection {
}
/// `POST /oauth2/token`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct OAuth2TokenEndpoint;
impl SimpleRoute for OAuth2TokenEndpoint {
@ -107,7 +107,7 @@ impl SimpleRoute for OAuth2TokenEndpoint {
}
/// `POST /oauth2/registration`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct OAuth2RegistrationEndpoint;
impl SimpleRoute for OAuth2RegistrationEndpoint {
@ -115,7 +115,7 @@ impl SimpleRoute for OAuth2RegistrationEndpoint {
}
/// `GET /authorize`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct OAuth2AuthorizationEndpoint;
impl SimpleRoute for OAuth2AuthorizationEndpoint {
@ -123,7 +123,7 @@ impl SimpleRoute for OAuth2AuthorizationEndpoint {
}
/// `GET /`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct Index;
impl SimpleRoute for Index {
@ -131,7 +131,7 @@ impl SimpleRoute for Index {
}
/// `GET /health`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct Healthcheck;
impl SimpleRoute for Healthcheck {
@ -200,7 +200,7 @@ impl From<Option<PostAuthAction>> for Login {
}
/// `POST /logout`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct Logout;
impl SimpleRoute for Logout {
@ -315,23 +315,23 @@ impl From<Option<PostAuthAction>> for Register {
}
}
/// `GET /verify/:code`
/// `GET /account/emails/verify/:id`
#[derive(Debug, Clone)]
pub struct VerifyEmail(pub String);
pub struct AccountVerifyEmail(pub i64);
impl Route for VerifyEmail {
impl Route for AccountVerifyEmail {
type Query = ();
fn route() -> &'static str {
"/verify/:code"
"/account/emails/verify/:id"
}
fn path(&self) -> std::borrow::Cow<'static, str> {
format!("/verify/{}", self.0).into()
format!("/account/emails/verify/{}", self.0).into()
}
}
/// `GET /account`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct Account;
impl SimpleRoute for Account {
@ -339,7 +339,7 @@ impl SimpleRoute for Account {
}
/// `GET|POST /account/password`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct AccountPassword;
impl SimpleRoute for AccountPassword {
@ -347,7 +347,7 @@ impl SimpleRoute for AccountPassword {
}
/// `GET|POST /account/emails`
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct AccountEmails;
impl SimpleRoute for AccountEmails {

View File

@ -91,25 +91,4 @@ impl UrlBuilder {
pub fn jwks_uri(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2Keys)
}
/// Email verification URL
#[must_use]
pub fn email_verification(&self, code: String) -> Url {
self.url_for(&crate::endpoints::VerifyEmail(code))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_email_verification_url() {
let base = Url::parse("https://example.com/").unwrap();
let builder = UrlBuilder::new(base);
assert_eq!(
builder.email_verification("123456abcdef".into()).as_str(),
"https://example.com/verify/123456abcdef"
);
}
}

View File

@ -669,6 +669,36 @@ pub async fn lookup_user_email(
Ok(res.into())
}
#[tracing::instrument(skip(executor))]
pub async fn lookup_user_email_by_id(
executor: impl PgExecutor<'_>,
user: &User<PostgresqlBackend>,
id: i64,
) -> anyhow::Result<UserEmail<PostgresqlBackend>> {
let res = sqlx::query_as!(
UserEmailLookup,
r#"
SELECT
ue.id AS "user_email_id",
ue.email AS "user_email",
ue.created_at AS "user_email_created_at",
ue.confirmed_at AS "user_email_confirmed_at"
FROM user_emails ue
WHERE ue.user_id = $1
AND ue.id = $2
"#,
user.data,
id,
)
.fetch_one(executor)
.instrument(info_span!("Lookup user email"))
.await
.context("could not lookup user email")?;
Ok(res.into())
}
#[tracing::instrument(skip(executor))]
pub async fn mark_user_email_as_verified(
executor: impl PgExecutor<'_>,
@ -695,18 +725,16 @@ pub async fn mark_user_email_as_verified(
struct UserEmailVerificationLookup {
verification_id: i64,
verification_code: String,
verification_expired: bool,
verification_created_at: DateTime<Utc>,
verification_consumed_at: Option<DateTime<Utc>>,
user_email_id: i64,
user_email: String,
user_email_created_at: DateTime<Utc>,
user_email_confirmed_at: Option<DateTime<Utc>>,
}
#[tracing::instrument(skip(executor))]
pub async fn lookup_user_email_verification_code(
executor: impl PgExecutor<'_>,
email: UserEmail<PostgresqlBackend>,
code: &str,
max_age: chrono::Duration,
) -> anyhow::Result<UserEmailVerification<PostgresqlBackend>> {
@ -720,19 +748,16 @@ pub async fn lookup_user_email_verification_code(
r#"
SELECT
ev.id AS "verification_id",
(ev.created_at + $2 < NOW()) AS "verification_expired!",
ev.code AS "verification_code",
(ev.created_at + $3 < NOW()) AS "verification_expired!",
ev.created_at AS "verification_created_at",
ev.consumed_at AS "verification_consumed_at",
ue.id AS "user_email_id",
ue.email AS "user_email",
ue.created_at AS "user_email_created_at",
ue.confirmed_at AS "user_email_confirmed_at"
ev.consumed_at AS "verification_consumed_at"
FROM user_email_verifications ev
INNER JOIN user_emails ue
ON ue.id = ev.user_email_id
WHERE ev.code = $1
AND ev.user_email_id = $2
"#,
code,
email.data,
max_age,
)
.fetch_one(executor)
@ -740,13 +765,6 @@ pub async fn lookup_user_email_verification_code(
.await
.context("could not lookup user email verification")?;
let email = UserEmail {
data: res.user_email_id,
email: res.user_email,
created_at: res.user_email_created_at,
confirmed_at: res.user_email_confirmed_at,
};
let state = if res.verification_expired {
UserEmailVerificationState::Expired
} else if let Some(when) = res.verification_consumed_at {
@ -757,6 +775,7 @@ pub async fn lookup_user_email_verification_code(
Ok(UserEmailVerification {
data: res.verification_id,
code: res.verification_code,
email,
state,
created_at: res.verification_created_at,
@ -794,21 +813,31 @@ pub async fn consume_email_verification(
#[tracing::instrument(skip(executor, email), fields(email.id = email.data, %email.email))]
pub async fn add_user_email_verification_code(
executor: impl PgExecutor<'_>,
email: &UserEmail<PostgresqlBackend>,
code: &str,
) -> anyhow::Result<()> {
sqlx::query!(
email: UserEmail<PostgresqlBackend>,
code: String,
) -> anyhow::Result<UserEmailVerification<PostgresqlBackend>> {
let res = sqlx::query_as!(
IdAndCreationTime,
r#"
INSERT INTO user_email_verifications (user_email_id, code)
VALUES ($1, $2)
RETURNING id, created_at
"#,
email.data,
code,
)
.execute(executor)
.fetch_one(executor)
.instrument(info_span!("Add user email verification code"))
.await
.context("could not insert user email verification code")?;
Ok(())
let verification = UserEmailVerification {
data: res.id,
email,
code,
created_at: res.created_at,
state: UserEmailVerificationState::Valid,
};
Ok(verification)
}

View File

@ -19,7 +19,7 @@
use chrono::Utc;
use mas_data_model::{
AuthorizationGrant, BrowserSession, CompatSsoLogin, CompatSsoLoginState, StorageBackend, User,
UserEmail,
UserEmail, UserEmailVerification,
};
use mas_router::PostAuthAction;
use serde::{ser::SerializeStruct, Deserialize, Serialize};
@ -562,21 +562,18 @@ impl<T: StorageBackend> TemplateContext for AccountEmailsContext<T> {
}
}
/// Context used by the `emails/verification.{txt,html}` templates
/// Context used by the `emails/verification.{txt,html,subject}` templates
#[derive(Serialize)]
pub struct EmailVerificationContext {
user: User<()>,
verification_link: Url,
verification: UserEmailVerification<()>,
}
impl EmailVerificationContext {
/// Constructs a context for the verification email
#[must_use]
pub fn new(user: User<()>, verification_link: Url) -> Self {
Self {
user,
verification_link,
}
pub fn new(user: User<()>, verification: UserEmailVerification<()>) -> Self {
Self { user, verification }
}
}
@ -587,16 +584,84 @@ impl TemplateContext for EmailVerificationContext {
{
User::samples()
.into_iter()
.map(|u| {
Self::new(
u,
Url::parse("https://example.com/emails/verify?code=2134").unwrap(),
)
.map(|user| {
let email = UserEmail {
data: (),
email: "foobar@example.com".to_string(),
created_at: Utc::now(),
confirmed_at: None,
};
let verification = UserEmailVerification {
data: (),
code: "123456".to_string(),
email,
created_at: Utc::now(),
state: mas_data_model::UserEmailVerificationState::Valid,
};
Self { user, verification }
})
.collect()
}
}
/// Fields of the email verification form
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EmailVerificationFormField {
/// The code field
Code,
}
impl FormField for EmailVerificationFormField {
fn keep(&self) -> bool {
match self {
Self::Code => true,
}
}
}
/// Context used by the `pages/account/verify.html` templates
#[derive(Serialize)]
pub struct EmailVerificationPageContext {
form: FormState<EmailVerificationFormField>,
email: UserEmail<()>,
}
impl EmailVerificationPageContext {
/// Constructs a context for the email verification page
#[must_use]
pub fn new<T>(email: T) -> Self
where
T: Into<UserEmail<()>>,
{
Self {
form: FormState::default(),
email: email.into(),
}
}
}
impl TemplateContext for EmailVerificationPageContext {
fn sample() -> Vec<Self>
where
Self: Sized,
{
let email = UserEmail {
data: (),
email: "foobar@example.com".to_string(),
created_at: Utc::now(),
confirmed_at: None,
};
vec![Self {
form: FormState::default(),
email,
}]
}
}
/// Context used by the `form_post.html` template
#[derive(Serialize)]
pub struct FormPostContext<T> {

View File

@ -46,10 +46,10 @@ mod macros;
pub use self::{
context::{
AccountContext, AccountEmailsContext, CompatSsoContext, ConsentContext,
EmailVerificationContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
LoginContext, LoginFormField, PostAuthContext, ReauthContext, ReauthFormField,
RegisterContext, RegisterFormField, TemplateContext, WithCsrf, WithOptionalSession,
WithSession,
EmailVerificationContext, EmailVerificationPageContext, EmptyContext, ErrorContext,
FormPostContext, IndexContext, LoginContext, LoginFormField, PostAuthContext,
ReauthContext, ReauthFormField, RegisterContext, RegisterFormField, TemplateContext,
WithCsrf, WithOptionalSession, WithSession,
},
forms::{FieldError, FormError, FormField, FormState, ToFormState},
};
@ -154,7 +154,7 @@ impl Templates {
// This uses blocking I/Os, do that in a blocking task
let tera = tokio::task::spawn_blocking(move || {
// Using `to_string_lossy` here is probably fine
let path = format!("{}/**/*.{{html,txt}}", root.to_string_lossy());
let path = format!("{}/**/*.{{html,txt,subject}}", root.to_string_lossy());
info!(%path, "Loading templates from filesystem");
Tera::parse(&path)
})
@ -326,11 +326,14 @@ register_templates! {
/// Render the email verification email (plain text variant)
pub fn render_email_verification_txt(EmailVerificationContext) { "emails/verification.txt" }
/// Render the email verification email (plain text variant)
/// Render the email verification email (HTML text variant)
pub fn render_email_verification_html(EmailVerificationContext) { "emails/verification.html" }
/// Render the email verification subject
pub fn render_email_verification_subject(EmailVerificationContext) { "emails/verification.subject" }
/// Render the email post-email verification page
pub fn render_email_verification_done(WithCsrf<WithOptionalSession<EmptyContext>>) { "pages/verify.html" }
pub fn render_email_verification_form(WithCsrf<WithSession<EmailVerificationPageContext>>) { "pages/account/verify.html" }
}
impl Templates {
@ -340,6 +343,7 @@ impl Templates {
check::render_login(self).await?;
check::render_register(self).await?;
check::render_consent(self).await?;
check::render_sso_login(self).await?;
check::render_index(self).await?;
check::render_account_index(self).await?;
check::render_account_password(self).await?;
@ -349,7 +353,8 @@ impl Templates {
check::render_error(self).await?;
check::render_email_verification_txt(self).await?;
check::render_email_verification_html(self).await?;
check::render_email_verification_done(self).await?;
check::render_email_verification_subject(self).await?;
check::render_email_verification_form(self).await?;
Ok(())
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
#}
{% macro input(label, name, type="text", form_state=false, autocomplete=false, class="") %}
{% macro input(label, name, type="text", form_state=false, autocomplete=false, class="", inputmode="text") %}
{% if not form_state %}
{% set form_state = dict(errors=[], fields=dict()) %}
{% endif %}
@ -30,13 +30,13 @@ limitations under the License.
{% endif %}
<label class="flex flex-col block {{ class }}">
<div class="mx-2 -mb-3 -mt-2 leading-5 px-1 z-10 self-start bg-white dark:bg-black-900 border-white border-1 dark:border-2 dark:border-black-900 rounded-full text-sm {{ text_color }}">{{ label }}</div>
<div
class="mx-2 -mb-3 -mt-2 leading-5 px-1 z-10 self-start bg-white dark:bg-black-900 border-white border-1 dark:border-2 dark:border-black-900 rounded-full text-sm {{ text_color }}">
{{ label }}</div>
<input name="{{ name }}"
class="z-0 px-3 py-2 bg-white dark:bg-black-900 rounded-lg {{ border_color }} border-1 dark:border-2 focus:border-accent focus:ring-0 focus:outline-0"
type="{{ type }}"
{% if autocomplete %} autocomplete="{{ autocomplete }}" {% endif %}
{% if state.value %} value="{{ state.value }}" {% endif %}
/>
type="{{ type }}" inputmode="{{ inputmode }}" {% if autocomplete %} autocomplete="{{ autocomplete }}" {% endif %} {%
if state.value %} value="{{ state.value }}" {% endif %} />
{% if state.errors is not empty %}
{% for error in state.errors %}

View File

@ -1,5 +1,5 @@
{#
Copyright 2021 The Matrix.org Foundation C.I.C.
Copyright 2021, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,8 +16,8 @@ limitations under the License.
Hi <b>{{ user.username }}</b>,<br />
<br />
click this link to verify your account:<br />
your email verification code is:
<br />
<a href="{{ verification_link }}">{{ verification_link }}</a><br />
<strong>{{ verification.code }}</strong><br />
<br />
kthxbye

View File

@ -14,12 +14,4 @@ See the License for the specific language governing permissions and
limitations under the License.
#}
{% extends "base.html" %}
{% block content %}
<section class="flex items-center justify-center flex-1">
<p class="font-bold text-xl">Email verified!</p>
</section>
{% endblock content %}
Your auth service verification code is: {{ verification.code }}

View File

@ -1,5 +1,5 @@
{#
Copyright 2021 The Matrix.org Foundation C.I.C.
Copyright 2021, 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,8 +16,8 @@ limitations under the License.
Hi {{ user.username }},
click this link to verify your account:
your email verification code is:
<{{ verification_link }}>
{{ verification.code }}
kthxbye

View File

@ -0,0 +1,42 @@
{#
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
#}
{% extends "base.html" %}
{% block content %}
{{ navbar::top() }}
<section class="flex items-center justify-center flex-1">
<form method="POST" class="grid grid-cols-1 gap-6 w-96 m-2">
<div class="text-center">
<h1 class="text-lg text-center font-medium">Email verification</h1>
<p>Please enter the 6-digit code sent to: <span class="font-bold">{{ email.email }}</span></p>
</div>
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-alert font-medium">
{{ errors::form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field::input(label="Code", name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }}
{{ button::button(text="Submit") }}
</section>
{% endblock content %}