You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
Implement the password change form
This commit is contained in:
@@ -389,6 +389,10 @@ where
|
|||||||
mas_router::AccountRecoveryProgress::route(),
|
mas_router::AccountRecoveryProgress::route(),
|
||||||
get(self::views::recovery::progress::get).post(self::views::recovery::progress::post),
|
get(self::views::recovery::progress::get).post(self::views::recovery::progress::post),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
mas_router::AccountRecoveryFinish::route(),
|
||||||
|
get(self::views::recovery::finish::get).post(self::views::recovery::finish::post),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
mas_router::OAuth2AuthorizationEndpoint::route(),
|
mas_router::OAuth2AuthorizationEndpoint::route(),
|
||||||
get(self::oauth2::authorization::get),
|
get(self::oauth2::authorization::get),
|
||||||
|
246
crates/handlers/src/views/recovery/finish.rs
Normal file
246
crates/handlers/src/views/recovery/finish.rs
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
// Copyright 2024 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 anyhow::Context;
|
||||||
|
use axum::{
|
||||||
|
extract::{Query, State},
|
||||||
|
response::{Html, IntoResponse, Response},
|
||||||
|
Form,
|
||||||
|
};
|
||||||
|
use mas_axum_utils::{
|
||||||
|
cookies::CookieJar,
|
||||||
|
csrf::{CsrfExt, ProtectedForm},
|
||||||
|
FancyError,
|
||||||
|
};
|
||||||
|
use mas_policy::Policy;
|
||||||
|
use mas_router::UrlBuilder;
|
||||||
|
use mas_storage::{BoxClock, BoxRepository, BoxRng};
|
||||||
|
use mas_templates::{
|
||||||
|
ErrorContext, FieldError, FormState, RecoveryFinishContext, RecoveryFinishFormField,
|
||||||
|
TemplateContext, Templates,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use crate::{passwords::PasswordManager, PreferredLanguage};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub(crate) struct RouteQuery {
|
||||||
|
ticket: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub(crate) struct RouteForm {
|
||||||
|
new_password: String,
|
||||||
|
new_password_confirm: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get(
|
||||||
|
mut rng: BoxRng,
|
||||||
|
clock: BoxClock,
|
||||||
|
mut repo: BoxRepository,
|
||||||
|
State(templates): State<Templates>,
|
||||||
|
PreferredLanguage(locale): PreferredLanguage,
|
||||||
|
cookie_jar: CookieJar,
|
||||||
|
Query(query): Query<RouteQuery>,
|
||||||
|
) -> Result<Response, FancyError> {
|
||||||
|
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||||
|
|
||||||
|
let ticket = repo
|
||||||
|
.user_recovery()
|
||||||
|
.find_ticket(&query.ticket)
|
||||||
|
.await?
|
||||||
|
.context("Unknown ticket")?;
|
||||||
|
|
||||||
|
let session = repo
|
||||||
|
.user_recovery()
|
||||||
|
.lookup_session(ticket.user_recovery_session_id)
|
||||||
|
.await?
|
||||||
|
.context("Unknown session")?;
|
||||||
|
|
||||||
|
if !ticket.active(clock.now()) || session.consumed_at.is_some() {
|
||||||
|
// TODO: render a 'link expired' page
|
||||||
|
let rendered = templates.render_error(
|
||||||
|
&ErrorContext::new()
|
||||||
|
.with_code("Link expired")
|
||||||
|
.with_language(&locale),
|
||||||
|
)?;
|
||||||
|
return Ok((cookie_jar, Html(rendered)).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_email = repo
|
||||||
|
.user_email()
|
||||||
|
.lookup(ticket.user_email_id)
|
||||||
|
.await?
|
||||||
|
// Only allow confirmed email addresses
|
||||||
|
.filter(|email| email.confirmed_at.is_some())
|
||||||
|
.context("Unknown email address")?;
|
||||||
|
|
||||||
|
let user = repo
|
||||||
|
.user()
|
||||||
|
.lookup(user_email.user_id)
|
||||||
|
.await?
|
||||||
|
.context("Invalid user")?;
|
||||||
|
|
||||||
|
if !user.is_valid() {
|
||||||
|
// TODO: render a 'account locked' page
|
||||||
|
let rendered = templates.render_error(
|
||||||
|
&ErrorContext::new()
|
||||||
|
.with_code("Account locked")
|
||||||
|
.with_language(&locale),
|
||||||
|
)?;
|
||||||
|
return Ok((cookie_jar, Html(rendered)).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = RecoveryFinishContext::new(user)
|
||||||
|
.with_csrf(csrf_token.form_value())
|
||||||
|
.with_language(locale);
|
||||||
|
|
||||||
|
let rendered = templates.render_recovery_finish(&context)?;
|
||||||
|
|
||||||
|
Ok((cookie_jar, Html(rendered)).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn post(
|
||||||
|
mut rng: BoxRng,
|
||||||
|
clock: BoxClock,
|
||||||
|
mut repo: BoxRepository,
|
||||||
|
mut policy: Policy,
|
||||||
|
State(password_manager): State<PasswordManager>,
|
||||||
|
State(templates): State<Templates>,
|
||||||
|
State(url_builder): State<UrlBuilder>,
|
||||||
|
PreferredLanguage(locale): PreferredLanguage,
|
||||||
|
cookie_jar: CookieJar,
|
||||||
|
Query(query): Query<RouteQuery>,
|
||||||
|
Form(form): Form<ProtectedForm<RouteForm>>,
|
||||||
|
) -> Result<Response, FancyError> {
|
||||||
|
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||||
|
|
||||||
|
let ticket = repo
|
||||||
|
.user_recovery()
|
||||||
|
.find_ticket(&query.ticket)
|
||||||
|
.await?
|
||||||
|
.context("Unknown ticket")?;
|
||||||
|
|
||||||
|
let session = repo
|
||||||
|
.user_recovery()
|
||||||
|
.lookup_session(ticket.user_recovery_session_id)
|
||||||
|
.await?
|
||||||
|
.context("Unknown session")?;
|
||||||
|
|
||||||
|
if !ticket.active(clock.now()) || session.consumed_at.is_some() {
|
||||||
|
// TODO: render a 'link expired' page
|
||||||
|
let rendered = templates.render_error(
|
||||||
|
&ErrorContext::new()
|
||||||
|
.with_code("Link expired")
|
||||||
|
.with_language(&locale),
|
||||||
|
)?;
|
||||||
|
return Ok((cookie_jar, Html(rendered)).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_email = repo
|
||||||
|
.user_email()
|
||||||
|
.lookup(ticket.user_email_id)
|
||||||
|
.await?
|
||||||
|
// Only allow confirmed email addresses
|
||||||
|
.filter(|email| email.confirmed_at.is_some())
|
||||||
|
.context("Unknown email address")?;
|
||||||
|
|
||||||
|
let user = repo
|
||||||
|
.user()
|
||||||
|
.lookup(user_email.user_id)
|
||||||
|
.await?
|
||||||
|
.context("Invalid user")?;
|
||||||
|
|
||||||
|
if !user.is_valid() {
|
||||||
|
// TODO: render a 'account locked' page
|
||||||
|
let rendered = templates.render_error(
|
||||||
|
&ErrorContext::new()
|
||||||
|
.with_code("Account locked")
|
||||||
|
.with_language(&locale),
|
||||||
|
)?;
|
||||||
|
return Ok((cookie_jar, Html(rendered)).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let form = cookie_jar.verify_form(&clock, form)?;
|
||||||
|
|
||||||
|
// Check the form
|
||||||
|
let mut form_state = FormState::from_form(&form);
|
||||||
|
|
||||||
|
if form.new_password.is_empty() {
|
||||||
|
form_state = form_state
|
||||||
|
.with_error_on_field(RecoveryFinishFormField::NewPassword, FieldError::Required);
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.new_password_confirm.is_empty() {
|
||||||
|
form_state = form_state.with_error_on_field(
|
||||||
|
RecoveryFinishFormField::NewPasswordConfirm,
|
||||||
|
FieldError::Required,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.new_password != form.new_password_confirm {
|
||||||
|
form_state = form_state
|
||||||
|
.with_error_on_field(
|
||||||
|
RecoveryFinishFormField::NewPassword,
|
||||||
|
FieldError::Unspecified,
|
||||||
|
)
|
||||||
|
.with_error_on_field(
|
||||||
|
RecoveryFinishFormField::NewPasswordConfirm,
|
||||||
|
FieldError::PasswordMismatch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = policy.evaluate_password(&form.new_password).await?;
|
||||||
|
|
||||||
|
if !res.valid() {
|
||||||
|
form_state = form_state.with_error_on_field(
|
||||||
|
RecoveryFinishFormField::NewPassword,
|
||||||
|
FieldError::Policy {
|
||||||
|
message: res.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !form_state.is_valid() {
|
||||||
|
let context = RecoveryFinishContext::new(user)
|
||||||
|
.with_form_state(form_state)
|
||||||
|
.with_csrf(csrf_token.form_value())
|
||||||
|
.with_language(locale);
|
||||||
|
|
||||||
|
let rendered = templates.render_recovery_finish(&context)?;
|
||||||
|
|
||||||
|
return Ok((cookie_jar, Html(rendered)).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form is valid, change the password
|
||||||
|
let password = Zeroizing::new(form.new_password.into_bytes());
|
||||||
|
let (version, hashed_password) = password_manager.hash(&mut rng, password).await?;
|
||||||
|
repo.user_password()
|
||||||
|
.add(&mut rng, &clock, &user, version, hashed_password, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Mark the session as consumed
|
||||||
|
repo.user_recovery()
|
||||||
|
.consume_ticket(&clock, ticket, session)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
repo.save().await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
cookie_jar,
|
||||||
|
url_builder.redirect(&mas_router::Login::default()),
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
|
}
|
@@ -12,5 +12,6 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
pub mod finish;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
pub mod start;
|
pub mod start;
|
||||||
|
@@ -191,9 +191,11 @@ pub(crate) async fn post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if form.password != form.password_confirm {
|
if form.password != form.password_confirm {
|
||||||
state.add_error_on_form(FormError::PasswordMismatch);
|
|
||||||
state.add_error_on_field(RegisterFormField::Password, FieldError::Unspecified);
|
state.add_error_on_field(RegisterFormField::Password, FieldError::Unspecified);
|
||||||
state.add_error_on_field(RegisterFormField::PasswordConfirm, FieldError::Unspecified);
|
state.add_error_on_field(
|
||||||
|
RegisterFormField::PasswordConfirm,
|
||||||
|
FieldError::PasswordMismatch,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the site has terms of service, the user must accept them
|
// If the site has terms of service, the user must accept them
|
||||||
|
@@ -1056,6 +1056,76 @@ impl TemplateContext for RecoveryProgressContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fields of the account recovery finish form
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum RecoveryFinishFormField {
|
||||||
|
/// The new password
|
||||||
|
NewPassword,
|
||||||
|
|
||||||
|
/// The new password confirmation
|
||||||
|
NewPasswordConfirm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormField for RecoveryFinishFormField {
|
||||||
|
fn keep(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context used by the `pages/recovery/finish.html` template
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RecoveryFinishContext {
|
||||||
|
user: User,
|
||||||
|
form: FormState<RecoveryFinishFormField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecoveryFinishContext {
|
||||||
|
/// Constructs a context for the recovery finish page
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(user: User) -> Self {
|
||||||
|
Self {
|
||||||
|
user,
|
||||||
|
form: FormState::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the form state
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
|
||||||
|
self.form = form;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateContext for RecoveryFinishContext {
|
||||||
|
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
User::samples(now, rng)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|user| {
|
||||||
|
vec![
|
||||||
|
Self::new(user.clone()),
|
||||||
|
Self::new(user.clone()).with_form_state(
|
||||||
|
FormState::default().with_error_on_field(
|
||||||
|
RecoveryFinishFormField::NewPassword,
|
||||||
|
FieldError::Invalid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Self::new(user.clone()).with_form_state(
|
||||||
|
FormState::default().with_error_on_field(
|
||||||
|
RecoveryFinishFormField::NewPasswordConfirm,
|
||||||
|
FieldError::Invalid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Context used by the `pages/upstream_oauth2/{link_mismatch,do_login}.html`
|
/// Context used by the `pages/upstream_oauth2/{link_mismatch,do_login}.html`
|
||||||
/// templates
|
/// templates
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@@ -36,6 +36,9 @@ pub enum FieldError {
|
|||||||
/// Invalid value for this field
|
/// Invalid value for this field
|
||||||
Invalid,
|
Invalid,
|
||||||
|
|
||||||
|
/// The password confirmation doesn't match the password
|
||||||
|
PasswordMismatch,
|
||||||
|
|
||||||
/// That value already exists
|
/// That value already exists
|
||||||
Exists,
|
Exists,
|
||||||
|
|
||||||
|
@@ -46,11 +46,12 @@ pub use self::{
|
|||||||
DeviceLinkFormField, EmailAddContext, EmailRecoveryContext, EmailVerificationContext,
|
DeviceLinkFormField, EmailAddContext, EmailRecoveryContext, EmailVerificationContext,
|
||||||
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
||||||
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
|
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
|
||||||
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryProgressContext,
|
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryFinishContext,
|
||||||
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
|
RecoveryFinishFormField, RecoveryProgressContext, RecoveryStartContext,
|
||||||
SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext, UpstreamExistingLinkContext,
|
RecoveryStartFormField, RegisterContext, RegisterFormField, SiteBranding, SiteConfigExt,
|
||||||
UpstreamRegister, UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf,
|
SiteFeatures, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister,
|
||||||
WithLanguage, WithOptionalSession, WithSession,
|
UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf, WithLanguage,
|
||||||
|
WithOptionalSession, WithSession,
|
||||||
},
|
},
|
||||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||||
};
|
};
|
||||||
@@ -353,6 +354,8 @@ register_templates! {
|
|||||||
/// Render the account recovery start page
|
/// Render the account recovery start page
|
||||||
pub fn render_recovery_progress(WithLanguage<WithCsrf<RecoveryProgressContext>>) { "pages/recovery/progress.html" }
|
pub fn render_recovery_progress(WithLanguage<WithCsrf<RecoveryProgressContext>>) { "pages/recovery/progress.html" }
|
||||||
|
|
||||||
|
/// Render the account recovery finish page
|
||||||
|
pub fn render_recovery_finish(WithLanguage<WithCsrf<RecoveryFinishContext>>) { "pages/recovery/finish.html" }
|
||||||
|
|
||||||
/// Render the re-authentication form
|
/// Render the re-authentication form
|
||||||
pub fn render_reauth(WithLanguage<WithCsrf<WithSession<ReauthContext>>>) { "pages/reauth.html" }
|
pub fn render_reauth(WithLanguage<WithCsrf<WithSession<ReauthContext>>>) { "pages/reauth.html" }
|
||||||
@@ -421,6 +424,7 @@ impl Templates {
|
|||||||
check::render_account_verify_email(self, now, rng)?;
|
check::render_account_verify_email(self, now, rng)?;
|
||||||
check::render_recovery_start(self, now, rng)?;
|
check::render_recovery_start(self, now, rng)?;
|
||||||
check::render_recovery_progress(self, now, rng)?;
|
check::render_recovery_progress(self, now, rng)?;
|
||||||
|
check::render_recovery_finish(self, now, rng)?;
|
||||||
check::render_reauth(self, now, rng)?;
|
check::render_reauth(self, now, rng)?;
|
||||||
check::render_form_post::<EmptyContext>(self, now, rng)?;
|
check::render_form_post::<EmptyContext>(self, now, rng)?;
|
||||||
check::render_error(self, now, rng)?;
|
check::render_error(self, now, rng)?;
|
||||||
|
@@ -70,6 +70,8 @@ limitations under the License.
|
|||||||
{{ _("mas.errors.username_taken") }}
|
{{ _("mas.errors.username_taken") }}
|
||||||
{% elif error.kind == "policy" %}
|
{% elif error.kind == "policy" %}
|
||||||
{{ _("mas.errors.denied_policy", policy=error.message) }}
|
{{ _("mas.errors.denied_policy", policy=error.message) }}
|
||||||
|
{% elif error.kind == "password_mismatch" %}
|
||||||
|
{{ _("mas.errors.password_mismatch") }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ error.kind }}
|
{{ error.kind }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
55
templates/pages/recovery/finish.html
Normal file
55
templates/pages/recovery/finish.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{#
|
||||||
|
Copyright 2024 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 %}
|
||||||
|
<header class="page-heading">
|
||||||
|
<div class="icon">
|
||||||
|
{{ icon.lock_solid() }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1 class="title">{{ _("mas.recovery.finish.heading") }}</h1>
|
||||||
|
<p class="text">{{ _("mas.recovery.finish.description") }}</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form class="cpd-form-root" method="POST">
|
||||||
|
{# Hidden username field so that password manager can save the username #}
|
||||||
|
<input class="hidden" aria-hidden="true" type="text" name="username" autocomplete="username" value="{{ user.username }}" />
|
||||||
|
|
||||||
|
{% if form.errors is not empty %}
|
||||||
|
{% for error in form.errors %}
|
||||||
|
<div class="text-critical font-medium">
|
||||||
|
{{ errors.form_error_message(error=error) }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
|
|
||||||
|
{% call(f) field.field(label=_("mas.recovery.finish.new"), name="new_password", form_state=form) %}
|
||||||
|
<input {{ field.attributes(f) }} class="cpd-text-control" type="password" autofocus autocomplete="new-password" required />
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% call(f) field.field(label=_("mas.recovery.finish.confirm"), name="new_password_confirm", form_state=form) %}
|
||||||
|
<input {{ field.attributes(f) }} class="cpd-text-control" type="password" autocomplete="new-password" required />
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{{ button.button(text=_("mas.recovery.finish.save_and_continue"), type="submit") }}
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
@@ -49,11 +49,11 @@ limitations under the License.
|
|||||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="email" autocomplete="email" required />
|
<input {{ field.attributes(f) }} class="cpd-text-control" type="email" autocomplete="email" required />
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call(f) field.field(label=_("common.password"), name="password") %}
|
{% call(f) field.field(label=_("common.password"), name="password", form_state=form) %}
|
||||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="password" autocomplete="new-password" required />
|
<input {{ field.attributes(f) }} class="cpd-text-control" type="password" autocomplete="new-password" required />
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
{% call(f) field.field(label=_("common.password_confirm"), name="password_confirm") %}
|
{% call(f) field.field(label=_("common.password_confirm"), name="password_confirm", form_state=form) %}
|
||||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="password" autocomplete="new-password" required />
|
<input {{ field.attributes(f) }} class="cpd-text-control" type="password" autocomplete="new-password" required />
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
|
@@ -284,7 +284,7 @@
|
|||||||
},
|
},
|
||||||
"password_mismatch": "Password fields don't match",
|
"password_mismatch": "Password fields don't match",
|
||||||
"@password_mismatch": {
|
"@password_mismatch": {
|
||||||
"context": "components/errors.html:21:7-40"
|
"context": "components/errors.html:21:7-40, components/field.html:74:17-50"
|
||||||
},
|
},
|
||||||
"username_taken": "This username is already taken",
|
"username_taken": "This username is already taken",
|
||||||
"@username_taken": {
|
"@username_taken": {
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
},
|
},
|
||||||
"or_separator": "Or",
|
"or_separator": "Or",
|
||||||
"@or_separator": {
|
"@or_separator": {
|
||||||
"context": "components/field.html:91:10-31",
|
"context": "components/field.html:93:10-31",
|
||||||
"description": "Separator between the login methods"
|
"description": "Separator between the login methods"
|
||||||
},
|
},
|
||||||
"policy_violation": {
|
"policy_violation": {
|
||||||
@@ -376,6 +376,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recovery": {
|
"recovery": {
|
||||||
|
"finish": {
|
||||||
|
"confirm": "Enter new password again",
|
||||||
|
"@confirm": {
|
||||||
|
"context": "pages/recovery/finish.html:49:33-65",
|
||||||
|
"description": "Label for the password confirmation field"
|
||||||
|
},
|
||||||
|
"description": "Choose a new password for your account.",
|
||||||
|
"@description": {
|
||||||
|
"context": "pages/recovery/finish.html:27:25-61",
|
||||||
|
"description": "Description for the final password recovery page"
|
||||||
|
},
|
||||||
|
"heading": "Reset your password",
|
||||||
|
"@heading": {
|
||||||
|
"context": "pages/recovery/finish.html:26:27-59",
|
||||||
|
"description": "Heading for the final password recovery page"
|
||||||
|
},
|
||||||
|
"new": "New password",
|
||||||
|
"@new": {
|
||||||
|
"context": "pages/recovery/finish.html:45:33-61",
|
||||||
|
"description": "Label for the new password field"
|
||||||
|
},
|
||||||
|
"save_and_continue": "Save and continue",
|
||||||
|
"@save_and_continue": {
|
||||||
|
"context": "pages/recovery/finish.html:53:26-68",
|
||||||
|
"description": "Button to save the new password and continue"
|
||||||
|
}
|
||||||
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"change_email": "Try a different email",
|
"change_email": "Try a different email",
|
||||||
"@change_email": {
|
"@change_email": {
|
||||||
|
Reference in New Issue
Block a user