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",
|
||||
"Reject member": "Reject member",
|
||||
"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",
|
||||
"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 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()">
|
||||
<b-field
|
||||
:label="$t('Email')"
|
||||
:type="errors.email ? 'is-danger' : null"
|
||||
:message="errors.email"
|
||||
:type="errorEmailType"
|
||||
:message="errorEmailMessages"
|
||||
label-for="email"
|
||||
>
|
||||
<b-input
|
||||
|
@ -100,8 +100,8 @@
|
|||
|
||||
<b-field
|
||||
:label="$t('Password')"
|
||||
:type="errors.password ? 'is-danger' : null"
|
||||
:message="errors.password"
|
||||
:type="errorPasswordType"
|
||||
:message="errorPasswordMessages"
|
||||
label-for="password"
|
||||
>
|
||||
<b-input
|
||||
|
@ -178,12 +178,6 @@
|
|||
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
|
@ -191,13 +185,18 @@
|
|||
</template>
|
||||
|
||||
<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 RouteName from "../../router/name";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import Subtitle from "../../components/Utils/Subtitle.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({
|
||||
components: { Subtitle, AuthProviders },
|
||||
|
@ -218,13 +217,14 @@ export default class Register extends Vue {
|
|||
|
||||
@Prop({ type: String, required: false, default: "" }) password!: string;
|
||||
|
||||
credentials = {
|
||||
credentials: credentials = {
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
locale: "en",
|
||||
};
|
||||
|
||||
errors: string[] = [];
|
||||
emailErrors: errorMessage[] = [];
|
||||
passwordErrors: errorMessage[] = [];
|
||||
|
||||
sendingForm = false;
|
||||
|
||||
|
@ -245,7 +245,8 @@ export default class Register extends Vue {
|
|||
this.sendingForm = true;
|
||||
this.credentials.locale = this.$i18n.locale;
|
||||
try {
|
||||
this.errors = [];
|
||||
this.emailErrors = [];
|
||||
this.passwordErrors = [];
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: CREATE_USER,
|
||||
|
@ -257,17 +258,67 @@ export default class Register extends Vue {
|
|||
params: { email: this.credentials.email },
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
this.errors = error.graphQLErrors.reduce(
|
||||
(acc: string[], localError: any) => {
|
||||
acc.push(localError.message);
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
error.graphQLErrors.forEach(
|
||||
({ field, message }: AbsintheGraphQLError) => {
|
||||
switch (field) {
|
||||
case "email":
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@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>
|
||||
|
||||
|
@ -302,4 +353,7 @@ p.create-account {
|
|||
margin: 1rem auto 2rem;
|
||||
}
|
||||
}
|
||||
::v-deep .help.is-warning {
|
||||
color: #755033;
|
||||
}
|
||||
</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()}
|
||||
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),
|
||||
{:ok, %User{} = user} <- Users.register(args),
|
||||
{:ok, %User{} = user} <- Users.register(%{args | email: email}),
|
||||
%Bamboo.Email{} <-
|
||||
Email.User.send_confirmation_email(user, Map.get(args, :locale, "en")) do
|
||||
{:ok, user}
|
||||
else
|
||||
{:error, :invalid_email} ->
|
||||
{:error, dgettext("errors", "Your email seems to be using an invalid format")}
|
||||
|
||||
:registration_closed ->
|
||||
{:error, dgettext("errors", "Registrations are not open")}
|
||||
|
||||
|
@ -190,24 +194,40 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
# Remove everything behind the +
|
||||
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,
|
||||
else: :not_deny_listed
|
||||
end
|
||||
|
||||
@spec check_allow_listed_email(String.t()) :: :registration_ok | :not_allowlisted
|
||||
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,
|
||||
else: :not_allowlisted
|
||||
end
|
||||
|
||||
defp email_in_list(email, list) do
|
||||
[_, domain] = String.split(email, "@", parts: 2, trim: true)
|
||||
@spec email_in_list?(String.t(), list(String.t())) :: boolean()
|
||||
defp email_in_list?(email, list) do
|
||||
[_, domain] = split_email(email)
|
||||
|
||||
domain in list or email in list
|
||||
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 """
|
||||
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], [])
|
||||
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
|
||||
conn
|
||||
|> put_req_header("accept-language", "fr")
|
||||
|
|
Loading…
Reference in a new issue