Warn when registering with email containing uppercase characters
Closes #884 and #803 Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
be1664ec85
commit
d291a83cc9
|
@ -1251,5 +1251,6 @@
|
||||||
"Approve member": "Approve member",
|
"Approve member": "Approve member",
|
||||||
"Reject member": "Reject member",
|
"Reject member": "Reject member",
|
||||||
"The membership request from {profile} was rejected": "The membership request from {profile} was rejected",
|
"The membership request from {profile} was rejected": "The membership request from {profile} was rejected",
|
||||||
"The member was approved": "The member was approved"
|
"The member was approved": "The member was approved",
|
||||||
|
"Emails usually don't contain capitals, make sure you haven't made a typo.": "Emails usually don't contain capitals, make sure you haven't made a typo."
|
||||||
}
|
}
|
||||||
|
|
|
@ -1355,5 +1355,6 @@
|
||||||
"Approve member": "Approuver le ou la membre",
|
"Approve member": "Approuver le ou la membre",
|
||||||
"Reject member": "Rejeter le ou la membre",
|
"Reject member": "Rejeter le ou la membre",
|
||||||
"The membership request from {profile} was rejected": "La demande d'adhésion de {profile} a été rejetée",
|
"The membership request from {profile} was rejected": "La demande d'adhésion de {profile} a été rejetée",
|
||||||
"The member was approved": "Le ou la membre a été approuvée"
|
"The member was approved": "Le ou la membre a été approuvée",
|
||||||
|
"Emails usually don't contain capitals, make sure you haven't made a typo.": "Les emails ne contiennent d'ordinaire pas de capitales, assurez-vous de n'avoir pas fait de faute de frappe."
|
||||||
}
|
}
|
||||||
|
|
5
js/src/types/apollo.ts
Normal file
5
js/src/types/apollo.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { GraphQLError } from "graphql/error/GraphQLError";
|
||||||
|
|
||||||
|
export class AbsintheGraphQLError extends GraphQLError {
|
||||||
|
readonly field: string | undefined;
|
||||||
|
}
|
|
@ -83,8 +83,8 @@
|
||||||
<form v-on:submit.prevent="submit()">
|
<form v-on:submit.prevent="submit()">
|
||||||
<b-field
|
<b-field
|
||||||
:label="$t('Email')"
|
:label="$t('Email')"
|
||||||
:type="errors.email ? 'is-danger' : null"
|
:type="errorEmailType"
|
||||||
:message="errors.email"
|
:message="errorEmailMessages"
|
||||||
label-for="email"
|
label-for="email"
|
||||||
>
|
>
|
||||||
<b-input
|
<b-input
|
||||||
|
@ -100,8 +100,8 @@
|
||||||
|
|
||||||
<b-field
|
<b-field
|
||||||
:label="$t('Password')"
|
:label="$t('Password')"
|
||||||
:type="errors.password ? 'is-danger' : null"
|
:type="errorPasswordType"
|
||||||
:message="errors.password"
|
:message="errorPasswordMessages"
|
||||||
label-for="password"
|
label-for="password"
|
||||||
>
|
>
|
||||||
<b-input
|
<b-input
|
||||||
|
@ -178,12 +178,6 @@
|
||||||
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div v-if="errors.length > 0">
|
|
||||||
<b-message type="is-danger" v-for="error in errors" :key="error">{{
|
|
||||||
error
|
|
||||||
}}</b-message>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -191,13 +185,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
import { CREATE_USER } from "../../graphql/user";
|
import { CREATE_USER } from "../../graphql/user";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import { IConfig } from "../../types/config.model";
|
import { IConfig } from "../../types/config.model";
|
||||||
import { CONFIG } from "../../graphql/config";
|
import { CONFIG } from "../../graphql/config";
|
||||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||||
import AuthProviders from "../../components/User/AuthProviders.vue";
|
import AuthProviders from "../../components/User/AuthProviders.vue";
|
||||||
|
import { AbsintheGraphQLError } from "../../types/apollo";
|
||||||
|
|
||||||
|
type errorType = "is-danger" | "is-warning";
|
||||||
|
type errorMessage = { type: errorType; message: string };
|
||||||
|
type credentials = { email: string; password: string; locale: string };
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { Subtitle, AuthProviders },
|
components: { Subtitle, AuthProviders },
|
||||||
|
@ -218,13 +217,14 @@ export default class Register extends Vue {
|
||||||
|
|
||||||
@Prop({ type: String, required: false, default: "" }) password!: string;
|
@Prop({ type: String, required: false, default: "" }) password!: string;
|
||||||
|
|
||||||
credentials = {
|
credentials: credentials = {
|
||||||
email: this.email,
|
email: this.email,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
locale: "en",
|
locale: "en",
|
||||||
};
|
};
|
||||||
|
|
||||||
errors: string[] = [];
|
emailErrors: errorMessage[] = [];
|
||||||
|
passwordErrors: errorMessage[] = [];
|
||||||
|
|
||||||
sendingForm = false;
|
sendingForm = false;
|
||||||
|
|
||||||
|
@ -245,7 +245,8 @@ export default class Register extends Vue {
|
||||||
this.sendingForm = true;
|
this.sendingForm = true;
|
||||||
this.credentials.locale = this.$i18n.locale;
|
this.credentials.locale = this.$i18n.locale;
|
||||||
try {
|
try {
|
||||||
this.errors = [];
|
this.emailErrors = [];
|
||||||
|
this.passwordErrors = [];
|
||||||
|
|
||||||
await this.$apollo.mutate({
|
await this.$apollo.mutate({
|
||||||
mutation: CREATE_USER,
|
mutation: CREATE_USER,
|
||||||
|
@ -257,17 +258,67 @@ export default class Register extends Vue {
|
||||||
params: { email: this.credentials.email },
|
params: { email: this.credentials.email },
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
error.graphQLErrors.forEach(
|
||||||
this.errors = error.graphQLErrors.reduce(
|
({ field, message }: AbsintheGraphQLError) => {
|
||||||
(acc: string[], localError: any) => {
|
switch (field) {
|
||||||
acc.push(localError.message);
|
case "email":
|
||||||
return acc;
|
this.emailErrors.push({
|
||||||
},
|
type: "is-danger" as errorType,
|
||||||
[]
|
message: message[0] as string,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "password":
|
||||||
|
this.passwordErrors.push({
|
||||||
|
type: "is-danger" as errorType,
|
||||||
|
message: message[0] as string,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.sendingForm = false;
|
this.sendingForm = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Watch("credentials", { deep: true })
|
||||||
|
watchCredentials(credentials: credentials): void {
|
||||||
|
if (credentials.email !== credentials.email.toLowerCase()) {
|
||||||
|
const error = {
|
||||||
|
type: "is-warning" as errorType,
|
||||||
|
message: this.$t(
|
||||||
|
"Emails usually don't contain capitals, make sure you haven't made a typo."
|
||||||
|
) as string,
|
||||||
|
};
|
||||||
|
this.emailErrors = [error];
|
||||||
|
this.$forceUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maxErrorType(errors: errorMessage[]): errorType | undefined {
|
||||||
|
if (!errors || errors.length === 0) return undefined;
|
||||||
|
return errors.reduce<errorType>((acc, error) => {
|
||||||
|
if (error.type === "is-danger" || acc === "is-danger") return "is-danger";
|
||||||
|
return "is-warning";
|
||||||
|
}, "is-warning");
|
||||||
|
}
|
||||||
|
|
||||||
|
get errorEmailType(): errorType | undefined {
|
||||||
|
return this.maxErrorType(this.emailErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
get errorPasswordType(): errorType | undefined {
|
||||||
|
return this.maxErrorType(this.passwordErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
get errorEmailMessages(): string[] {
|
||||||
|
return this.emailErrors.map(({ message }) => message);
|
||||||
|
}
|
||||||
|
|
||||||
|
get errorPasswordMessages(): string[] {
|
||||||
|
return this.passwordErrors?.map(({ message }) => message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -302,4 +353,7 @@ p.create-account {
|
||||||
margin: 1rem auto 2rem;
|
margin: 1rem auto 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
::v-deep .help.is-warning {
|
||||||
|
color: #755033;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -145,13 +145,17 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||||
"""
|
"""
|
||||||
@spec create_user(any, %{email: String.t()}, any) :: {:ok, User.t()} | {:error, String.t()}
|
@spec create_user(any, %{email: String.t()}, any) :: {:ok, User.t()} | {:error, String.t()}
|
||||||
def create_user(_parent, %{email: email} = args, _resolution) do
|
def create_user(_parent, %{email: email} = args, _resolution) do
|
||||||
with :registration_ok <- check_registration_config(email),
|
with {:ok, email} <- lowercase_domain(email),
|
||||||
|
:registration_ok <- check_registration_config(email),
|
||||||
:not_deny_listed <- check_registration_denylist(email),
|
:not_deny_listed <- check_registration_denylist(email),
|
||||||
{:ok, %User{} = user} <- Users.register(args),
|
{:ok, %User{} = user} <- Users.register(%{args | email: email}),
|
||||||
%Bamboo.Email{} <-
|
%Bamboo.Email{} <-
|
||||||
Email.User.send_confirmation_email(user, Map.get(args, :locale, "en")) do
|
Email.User.send_confirmation_email(user, Map.get(args, :locale, "en")) do
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
else
|
else
|
||||||
|
{:error, :invalid_email} ->
|
||||||
|
{:error, dgettext("errors", "Your email seems to be using an invalid format")}
|
||||||
|
|
||||||
:registration_closed ->
|
:registration_closed ->
|
||||||
{:error, dgettext("errors", "Registrations are not open")}
|
{:error, dgettext("errors", "Registrations are not open")}
|
||||||
|
|
||||||
|
@ -190,24 +194,40 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||||
# Remove everything behind the +
|
# Remove everything behind the +
|
||||||
email = String.replace(email, ~r/(\+.*)(?=\@)/, "")
|
email = String.replace(email, ~r/(\+.*)(?=\@)/, "")
|
||||||
|
|
||||||
if email_in_list(email, Config.instance_registrations_denylist()),
|
if email_in_list?(email, Config.instance_registrations_denylist()),
|
||||||
do: :deny_listed,
|
do: :deny_listed,
|
||||||
else: :not_deny_listed
|
else: :not_deny_listed
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec check_allow_listed_email(String.t()) :: :registration_ok | :not_allowlisted
|
@spec check_allow_listed_email(String.t()) :: :registration_ok | :not_allowlisted
|
||||||
defp check_allow_listed_email(email) do
|
defp check_allow_listed_email(email) do
|
||||||
if email_in_list(email, Config.instance_registrations_allowlist()),
|
if email_in_list?(email, Config.instance_registrations_allowlist()),
|
||||||
do: :registration_ok,
|
do: :registration_ok,
|
||||||
else: :not_allowlisted
|
else: :not_allowlisted
|
||||||
end
|
end
|
||||||
|
|
||||||
defp email_in_list(email, list) do
|
@spec email_in_list?(String.t(), list(String.t())) :: boolean()
|
||||||
[_, domain] = String.split(email, "@", parts: 2, trim: true)
|
defp email_in_list?(email, list) do
|
||||||
|
[_, domain] = split_email(email)
|
||||||
|
|
||||||
domain in list or email in list
|
domain in list or email in list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Domains should always be lower-case, so let's force that
|
||||||
|
@spec lowercase_domain(String.t()) :: {:ok, String.t()} | {:error, :invalid_email}
|
||||||
|
defp lowercase_domain(email) do
|
||||||
|
case split_email(email) do
|
||||||
|
[user_part, domain_part] ->
|
||||||
|
{:ok, "#{user_part}@#{String.downcase(domain_part)}"}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :invalid_email}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec split_email(String.t()) :: list(String.t())
|
||||||
|
defp split_email(email), do: String.split(email, "@", parts: 2, trim: true)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Validate an user, get its actor and a token
|
Validate an user, get its actor and a token
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -454,6 +454,21 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||||
Config.put([:instance, :registration_email_denylist], [])
|
Config.put([:instance, :registration_email_denylist], [])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "create_user/3 lowers domain part of email",
|
||||||
|
%{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @create_user_mutation,
|
||||||
|
variables: Map.put(@user_creation, :email, "test+alias@DEMO.tld")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res["errors"] == nil
|
||||||
|
assert res["data"]["createUser"]["email"] == "test+alias@demo.tld"
|
||||||
|
end
|
||||||
|
|
||||||
test "register_person/3 doesn't register a profile from an unknown email", %{conn: conn} do
|
test "register_person/3 doesn't register a profile from an unknown email", %{conn: conn} do
|
||||||
conn
|
conn
|
||||||
|> put_req_header("accept-language", "fr")
|
|> put_req_header("accept-language", "fr")
|
||||||
|
|
Loading…
Reference in a new issue