You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-09 04:22:45 +03:00
Make the user agree to T&C during registration
This commit is contained in:
@@ -160,6 +160,7 @@ impl Options {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let site_config = SiteConfig {
|
let site_config = SiteConfig {
|
||||||
|
tos_uri: config.branding.tos_uri.clone(),
|
||||||
access_token_ttl: config.experimental.access_token_ttl,
|
access_token_ttl: config.experimental.access_token_ttl,
|
||||||
compat_token_ttl: config.experimental.compat_token_ttl,
|
compat_token_ttl: config.experimental.compat_token_ttl,
|
||||||
};
|
};
|
||||||
|
@@ -319,6 +319,7 @@ where
|
|||||||
HttpClientFactory: FromRef<S>,
|
HttpClientFactory: FromRef<S>,
|
||||||
PasswordManager: FromRef<S>,
|
PasswordManager: FromRef<S>,
|
||||||
MetadataCache: FromRef<S>,
|
MetadataCache: FromRef<S>,
|
||||||
|
SiteConfig: FromRef<S>,
|
||||||
BoxClock: FromRequestParts<S>,
|
BoxClock: FromRequestParts<S>,
|
||||||
BoxRng: FromRequestParts<S>,
|
BoxRng: FromRequestParts<S>,
|
||||||
Policy: FromRequestParts<S>,
|
Policy: FromRequestParts<S>,
|
||||||
|
@@ -13,12 +13,14 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
/// Random site configuration we don't now where to put yet.
|
/// Random site configuration we don't now where to put yet.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SiteConfig {
|
pub struct SiteConfig {
|
||||||
pub access_token_ttl: Duration,
|
pub access_token_ttl: Duration,
|
||||||
pub compat_token_ttl: Duration,
|
pub compat_token_ttl: Duration,
|
||||||
|
pub tos_uri: Option<Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SiteConfig {
|
impl Default for SiteConfig {
|
||||||
@@ -26,6 +28,7 @@ impl Default for SiteConfig {
|
|||||||
Self {
|
Self {
|
||||||
access_token_ttl: Duration::minutes(5),
|
access_token_ttl: Duration::minutes(5),
|
||||||
compat_token_ttl: Duration::minutes(5),
|
compat_token_ttl: Duration::minutes(5),
|
||||||
|
tos_uri: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -119,7 +119,9 @@ impl TestState {
|
|||||||
|
|
||||||
let url_builder = UrlBuilder::new("https://example.com/".parse()?, None, None);
|
let url_builder = UrlBuilder::new("https://example.com/".parse()?, None, None);
|
||||||
|
|
||||||
let site_branding = SiteBranding::new("example.com").with_service_name("Example");
|
let site_branding = SiteBranding::new("example.com")
|
||||||
|
.with_service_name("Example")
|
||||||
|
.with_tos_uri("https://example.com/tos");
|
||||||
|
|
||||||
let templates = Templates::load(
|
let templates = Templates::load(
|
||||||
workspace_root.join("templates"),
|
workspace_root.join("templates"),
|
||||||
@@ -154,7 +156,10 @@ impl TestState {
|
|||||||
|
|
||||||
let http_client_factory = HttpClientFactory::new().await?;
|
let http_client_factory = HttpClientFactory::new().await?;
|
||||||
|
|
||||||
let site_config = SiteConfig::default();
|
let site_config = SiteConfig {
|
||||||
|
tos_uri: Some("https://example.com/tos".parse().unwrap()),
|
||||||
|
..SiteConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
let clock = Arc::new(MockClock::default());
|
let clock = Arc::new(MockClock::default());
|
||||||
let rng = Arc::new(Mutex::new(ChaChaRng::seed_from_u64(42)));
|
let rng = Arc::new(Mutex::new(ChaChaRng::seed_from_u64(42)));
|
||||||
|
@@ -45,7 +45,9 @@ use tracing::warn;
|
|||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
use super::{template::environment, UpstreamSessionsCookie};
|
use super::{template::environment, UpstreamSessionsCookie};
|
||||||
use crate::{impl_from_error_for_route, views::shared::OptionalPostAuthAction, PreferredLanguage};
|
use crate::{
|
||||||
|
impl_from_error_for_route, views::shared::OptionalPostAuthAction, PreferredLanguage, SiteConfig,
|
||||||
|
};
|
||||||
|
|
||||||
const DEFAULT_LOCALPART_TEMPLATE: &str = "{{ user.preferred_username }}";
|
const DEFAULT_LOCALPART_TEMPLATE: &str = "{{ user.preferred_username }}";
|
||||||
const DEFAULT_DISPLAYNAME_TEMPLATE: &str = "{{ user.name }}";
|
const DEFAULT_DISPLAYNAME_TEMPLATE: &str = "{{ user.name }}";
|
||||||
@@ -170,6 +172,8 @@ pub(crate) enum FormData {
|
|||||||
import_email: Option<String>,
|
import_email: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
import_display_name: Option<String>,
|
import_display_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
accept_terms: Option<String>,
|
||||||
},
|
},
|
||||||
Link,
|
Link,
|
||||||
}
|
}
|
||||||
@@ -473,6 +477,7 @@ pub(crate) async fn post(
|
|||||||
PreferredLanguage(locale): PreferredLanguage,
|
PreferredLanguage(locale): PreferredLanguage,
|
||||||
State(templates): State<Templates>,
|
State(templates): State<Templates>,
|
||||||
State(url_builder): State<UrlBuilder>,
|
State(url_builder): State<UrlBuilder>,
|
||||||
|
State(site_config): State<SiteConfig>,
|
||||||
Path(link_id): Path<Ulid>,
|
Path(link_id): Path<Ulid>,
|
||||||
Form(form): Form<ProtectedForm<FormData>>,
|
Form(form): Form<ProtectedForm<FormData>>,
|
||||||
) -> Result<Response, RouteError> {
|
) -> Result<Response, RouteError> {
|
||||||
@@ -533,6 +538,7 @@ pub(crate) async fn post(
|
|||||||
username,
|
username,
|
||||||
import_email,
|
import_email,
|
||||||
import_display_name,
|
import_display_name,
|
||||||
|
accept_terms,
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
// The user got the form to register a new account, and is not logged in.
|
// The user got the form to register a new account, and is not logged in.
|
||||||
@@ -543,6 +549,7 @@ pub(crate) async fn post(
|
|||||||
// Those fields are Some("on") if the checkbox is checked
|
// Those fields are Some("on") if the checkbox is checked
|
||||||
let import_email = import_email.is_some();
|
let import_email = import_email.is_some();
|
||||||
let import_display_name = import_display_name.is_some();
|
let import_display_name = import_display_name.is_some();
|
||||||
|
let accept_terms = accept_terms.is_some();
|
||||||
|
|
||||||
let id_token = upstream_session
|
let id_token = upstream_session
|
||||||
.id_token()
|
.id_token()
|
||||||
@@ -695,6 +702,24 @@ pub(crate) async fn post(
|
|||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we need have a TOS in the config, make sure the user has accepted it
|
||||||
|
if site_config.tos_uri.is_some() && !accept_terms {
|
||||||
|
let form_state = form_state.with_error_on_field(
|
||||||
|
mas_templates::UpstreamRegisterFormField::AcceptTerms,
|
||||||
|
FieldError::Required,
|
||||||
|
);
|
||||||
|
|
||||||
|
let ctx = ctx
|
||||||
|
.with_form_state(form_state)
|
||||||
|
.with_csrf(csrf_token.form_value())
|
||||||
|
.with_language(locale);
|
||||||
|
return Ok((
|
||||||
|
cookie_jar,
|
||||||
|
Html(templates.render_upstream_oauth2_do_register(&ctx)?),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
// Policy check
|
// Policy check
|
||||||
let res = policy
|
let res = policy
|
||||||
.evaluate_upstream_oauth_register(&username, email.as_deref())
|
.evaluate_upstream_oauth_register(&username, email.as_deref())
|
||||||
@@ -731,6 +756,12 @@ pub(crate) async fn post(
|
|||||||
// Now we can create the user
|
// Now we can create the user
|
||||||
let user = repo.user().add(&mut rng, &clock, username).await?;
|
let user = repo.user().add(&mut rng, &clock, username).await?;
|
||||||
|
|
||||||
|
if let Some(terms_url) = &site_config.tos_uri {
|
||||||
|
repo.user_terms()
|
||||||
|
.accept_terms(&mut rng, &clock, &user, terms_url.clone())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
// And schedule the job to provision it
|
// And schedule the job to provision it
|
||||||
let mut job = ProvisionUserJob::new(&user);
|
let mut job = ProvisionUserJob::new(&user);
|
||||||
|
|
||||||
@@ -936,6 +967,7 @@ mod tests {
|
|||||||
"csrf": csrf_token,
|
"csrf": csrf_token,
|
||||||
"action": "register",
|
"action": "register",
|
||||||
"import_email": "on",
|
"import_email": "on",
|
||||||
|
"accept_terms": "on",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let request = cookies.with_cookies(request);
|
let request = cookies.with_cookies(request);
|
||||||
|
@@ -43,7 +43,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
use super::shared::OptionalPostAuthAction;
|
use super::shared::OptionalPostAuthAction;
|
||||||
use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage};
|
use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage, SiteConfig};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub(crate) struct RegisterForm {
|
pub(crate) struct RegisterForm {
|
||||||
@@ -51,6 +51,8 @@ pub(crate) struct RegisterForm {
|
|||||||
email: String,
|
email: String,
|
||||||
password: String,
|
password: String,
|
||||||
password_confirm: String,
|
password_confirm: String,
|
||||||
|
#[serde(default)]
|
||||||
|
accept_terms: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToFormState for RegisterForm {
|
impl ToFormState for RegisterForm {
|
||||||
@@ -108,6 +110,7 @@ pub(crate) async fn post(
|
|||||||
State(password_manager): State<PasswordManager>,
|
State(password_manager): State<PasswordManager>,
|
||||||
State(templates): State<Templates>,
|
State(templates): State<Templates>,
|
||||||
State(url_builder): State<UrlBuilder>,
|
State(url_builder): State<UrlBuilder>,
|
||||||
|
State(site_config): State<SiteConfig>,
|
||||||
mut policy: Policy,
|
mut policy: Policy,
|
||||||
mut repo: BoxRepository,
|
mut repo: BoxRepository,
|
||||||
activity_tracker: BoundActivityTracker,
|
activity_tracker: BoundActivityTracker,
|
||||||
@@ -155,6 +158,11 @@ pub(crate) async fn post(
|
|||||||
state.add_error_on_field(RegisterFormField::PasswordConfirm, FieldError::Unspecified);
|
state.add_error_on_field(RegisterFormField::PasswordConfirm, FieldError::Unspecified);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the site has terms of service, the user must accept them
|
||||||
|
if site_config.tos_uri.is_some() && form.accept_terms != "on" {
|
||||||
|
state.add_error_on_field(RegisterFormField::AcceptTerms, FieldError::Required);
|
||||||
|
}
|
||||||
|
|
||||||
let res = policy
|
let res = policy
|
||||||
.evaluate_register(&form.username, &form.password, &form.email)
|
.evaluate_register(&form.username, &form.password, &form.email)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -203,6 +211,13 @@ pub(crate) async fn post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let user = repo.user().add(&mut rng, &clock, form.username).await?;
|
let user = repo.user().add(&mut rng, &clock, form.username).await?;
|
||||||
|
|
||||||
|
if let Some(tos_uri) = &site_config.tos_uri {
|
||||||
|
repo.user_terms()
|
||||||
|
.accept_terms(&mut rng, &clock, &user, tos_uri.clone())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let password = Zeroizing::new(form.password.into_bytes());
|
let password = Zeroizing::new(form.password.into_bytes());
|
||||||
let (version, hashed_password) = password_manager.hash(&mut rng, password).await?;
|
let (version, hashed_password) = password_manager.hash(&mut rng, password).await?;
|
||||||
let user_password = repo
|
let user_password = repo
|
||||||
|
@@ -19,7 +19,8 @@ use url::Url;
|
|||||||
|
|
||||||
use crate::{repository_impl, Clock};
|
use crate::{repository_impl, Clock};
|
||||||
|
|
||||||
/// A [`UserTermsRepository`] helps interacting with the terms of service agreed by a [`User`]
|
/// A [`UserTermsRepository`] helps interacting with the terms of service agreed
|
||||||
|
/// by a [`User`]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait UserTermsRepository: Send + Sync {
|
pub trait UserTermsRepository: Send + Sync {
|
||||||
/// The error type returned by the repository
|
/// The error type returned by the repository
|
||||||
|
@@ -493,12 +493,15 @@ pub enum RegisterFormField {
|
|||||||
|
|
||||||
/// The password confirmation field
|
/// The password confirmation field
|
||||||
PasswordConfirm,
|
PasswordConfirm,
|
||||||
|
|
||||||
|
/// The terms of service agreement field
|
||||||
|
AcceptTerms,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FormField for RegisterFormField {
|
impl FormField for RegisterFormField {
|
||||||
fn keep(&self) -> bool {
|
fn keep(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Username | Self::Email => true,
|
Self::Username | Self::Email | Self::AcceptTerms => true,
|
||||||
Self::Password | Self::PasswordConfirm => false,
|
Self::Password | Self::PasswordConfirm => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -974,12 +977,15 @@ impl TemplateContext for UpstreamSuggestLink {
|
|||||||
pub enum UpstreamRegisterFormField {
|
pub enum UpstreamRegisterFormField {
|
||||||
/// The username field
|
/// The username field
|
||||||
Username,
|
Username,
|
||||||
|
|
||||||
|
/// Accept the terms of service
|
||||||
|
AcceptTerms,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FormField for UpstreamRegisterFormField {
|
impl FormField for UpstreamRegisterFormField {
|
||||||
fn keep(&self) -> bool {
|
fn keep(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Username => true,
|
Self::Username | Self::AcceptTerms => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@ limitations under the License.
|
|||||||
{%- if value %} value="{{ value }}" {% endif %}
|
{%- if value %} value="{{ value }}" {% endif %}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro field(label, name, form_state=false, class="") %}
|
{% macro field(label, name, form_state=false, class="", inline=false) %}
|
||||||
{% set field_id = new_id() %}
|
{% set field_id = new_id() %}
|
||||||
{% if not form_state %}
|
{% if not form_state %}
|
||||||
{% set form_state = {"fields": {}} %}
|
{% set form_state = {"fields": {}} %}
|
||||||
@@ -41,12 +41,24 @@ limitations under the License.
|
|||||||
"value": state.value,
|
"value": state.value,
|
||||||
} %}
|
} %}
|
||||||
|
|
||||||
<div class="cpd-form-field {{ class }}">
|
<div class="{% if inline %}cpd-form-inline-field{% else %}cpd-form-field{% endif %} {{ class }}">
|
||||||
<label class="cpd-form-label" for="{{ field.id }}"
|
{% if not inline %}
|
||||||
{%- if field.errors is not empty %} data-invalid{% endif -%}
|
<label class="cpd-form-label" for="{{ field.id }}"
|
||||||
>{{ label }}</label>
|
{%- if field.errors is not empty %} data-invalid{% endif -%}
|
||||||
|
>{{ label }}</label>
|
||||||
|
|
||||||
|
{{ caller(field) }}
|
||||||
|
{% else %}
|
||||||
|
<div class="cpd-form-inline-field-control">
|
||||||
|
{{ caller(field) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cpd-form-inline-field-body">
|
||||||
|
<label class="cpd-form-label" for="{{ field.id }}"
|
||||||
|
{%- if field.errors is not empty %} data-invalid{% endif -%}
|
||||||
|
>{{ label }}</label>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ caller(field) }}
|
|
||||||
|
|
||||||
{% if field.errors is not empty %}
|
{% if field.errors is not empty %}
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
@@ -65,6 +77,10 @@ limitations under the License.
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if inline %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
@@ -56,6 +56,19 @@ limitations under the License.
|
|||||||
<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 %}
|
||||||
|
|
||||||
|
{% if branding.tos_uri %}
|
||||||
|
{% call(f) field.field(label=_("mas.register.terms_of_service", tos_uri=branding.tos_uri), name="accept_terms", form_state=form, inline=true, class="my-4") %}
|
||||||
|
<div class="cpd-form-inline-field-control">
|
||||||
|
<div class="cpd-checkbox-container">
|
||||||
|
<input {{ field.attributes(f) }} class="cpd-checkbox-input" type="checkbox" required />
|
||||||
|
<div class="cpd-checkbox-ui">
|
||||||
|
{{ icon.check() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ button.button(text=_("action.continue")) }}
|
{{ button.button(text=_("action.continue")) }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@@ -140,6 +140,20 @@ limitations under the License.
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if branding.tos_uri %}
|
||||||
|
{% call(f) field.field(label=_("mas.register.terms_of_service", tos_uri=branding.tos_uri), name="accept_terms", form_state=form_state, inline=true, class="my-4") %}
|
||||||
|
<div class="cpd-form-inline-field-control">
|
||||||
|
<div class="cpd-checkbox-container">
|
||||||
|
<input {{ field.attributes(f) }} class="cpd-checkbox-input" type="checkbox" required />
|
||||||
|
<div class="cpd-checkbox-ui">
|
||||||
|
{{ icon.check() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{{ button.button(text=_("action.create_account")) }}
|
{{ button.button(text=_("action.create_account")) }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@@ -2,15 +2,15 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"@cancel": {
|
"@cancel": {
|
||||||
"context": "pages/consent.html:72:11-29, pages/device_consent.html:94:13-31, pages/login.html:100:13-31, pages/policy_violation.html:52:13-31, pages/register.html:64:13-31"
|
"context": "pages/consent.html:72:11-29, pages/device_consent.html:94:13-31, pages/login.html:100:13-31, pages/policy_violation.html:52:13-31, pages/register.html:77:13-31"
|
||||||
},
|
},
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"@continue": {
|
"@continue": {
|
||||||
"context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:60:28-48, pages/device_consent.html:91:13-33, pages/device_link.html:50:26-46, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/register.html:59:28-48, pages/sso.html:45:28-48"
|
"context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:60:28-48, pages/device_consent.html:91:13-33, pages/device_link.html:50:26-46, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/register.html:72:28-48, pages/sso.html:45:28-48"
|
||||||
},
|
},
|
||||||
"create_account": "Create Account",
|
"create_account": "Create Account",
|
||||||
"@create_account": {
|
"@create_account": {
|
||||||
"context": "pages/login.html:72:35-61, pages/upstream_oauth2/do_register.html:143:26-52"
|
"context": "pages/login.html:72:35-61, pages/upstream_oauth2/do_register.html:157:26-52"
|
||||||
},
|
},
|
||||||
"sign_in": "Sign in",
|
"sign_in": "Sign in",
|
||||||
"@sign_in": {
|
"@sign_in": {
|
||||||
@@ -167,11 +167,11 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"denied_policy": "Denied by policy: %(policy)s",
|
"denied_policy": "Denied by policy: %(policy)s",
|
||||||
"@denied_policy": {
|
"@denied_policy": {
|
||||||
"context": "components/errors.html:23:7-58, components/field.html:60:17-68"
|
"context": "components/errors.html:23:7-58, components/field.html:72:17-68"
|
||||||
},
|
},
|
||||||
"field_required": "This field is required",
|
"field_required": "This field is required",
|
||||||
"@field_required": {
|
"@field_required": {
|
||||||
"context": "components/field.html:56:17-47"
|
"context": "components/field.html:68:17-47"
|
||||||
},
|
},
|
||||||
"invalid_credentials": "Invalid credentials",
|
"invalid_credentials": "Invalid credentials",
|
||||||
"@invalid_credentials": {
|
"@invalid_credentials": {
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
},
|
},
|
||||||
"username_taken": "This username is already taken",
|
"username_taken": "This username is already taken",
|
||||||
"@username_taken": {
|
"@username_taken": {
|
||||||
"context": "components/field.html:58:17-47"
|
"context": "components/field.html:70:17-47"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
@@ -251,7 +251,7 @@
|
|||||||
},
|
},
|
||||||
"or_separator": "Or",
|
"or_separator": "Or",
|
||||||
"@or_separator": {
|
"@or_separator": {
|
||||||
"context": "components/field.html:75:10-31",
|
"context": "components/field.html:91:10-31",
|
||||||
"description": "Separator between the login methods"
|
"description": "Separator between the login methods"
|
||||||
},
|
},
|
||||||
"policy_violation": {
|
"policy_violation": {
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
"register": {
|
"register": {
|
||||||
"call_to_login": "Already have an account?",
|
"call_to_login": "Already have an account?",
|
||||||
"@call_to_login": {
|
"@call_to_login": {
|
||||||
"context": "pages/register.html:74:11-42",
|
"context": "pages/register.html:87:11-42",
|
||||||
"description": "Displayed on the registration page to suggest to log in instead"
|
"description": "Displayed on the registration page to suggest to log in instead"
|
||||||
},
|
},
|
||||||
"create_account": {
|
"create_account": {
|
||||||
@@ -288,7 +288,11 @@
|
|||||||
},
|
},
|
||||||
"sign_in_instead": "Sign in instead",
|
"sign_in_instead": "Sign in instead",
|
||||||
"@sign_in_instead": {
|
"@sign_in_instead": {
|
||||||
"context": "pages/register.html:78:31-64"
|
"context": "pages/register.html:91:31-64"
|
||||||
|
},
|
||||||
|
"terms_of_service": "I agree to the <a href=\"%s\" data-kind=\"primary\" class=\"cpd-link\">Terms and Conditions</a>",
|
||||||
|
"@terms_of_service": {
|
||||||
|
"context": "pages/register.html:60:37-97, pages/upstream_oauth2/do_register.html:144:35-95"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
|
Reference in New Issue
Block a user