diff --git a/lib/graphql/resolvers/user.ex b/lib/graphql/resolvers/user.ex index 55330e67b..15ce54e63 100644 --- a/lib/graphql/resolvers/user.ex +++ b/lib/graphql/resolvers/user.ex @@ -153,13 +153,13 @@ defmodule Mobilizon.GraphQL.Resolvers.User do - send a validation email to the user """ @spec create_user(any, %{email: String.t()}, any) :: {:ok, User.t()} | {:error, String.t()} - def create_user(_parent, %{email: email} = args, %{context: context}) do + def create_user(_parent, %{email: email, moderation: moderation} = args, %{context: context}) do current_ip = Map.get(context, :ip) user_agent = Map.get(context, :user_agent, "") now = DateTime.utc_now() with {:ok, email} <- lowercase_domain(email), - :registration_ok <- check_registration_config(email), + :registration_ok <- check_registration_config(email, moderation), :not_deny_listed <- check_registration_denylist(email), {:spam, :ham} <- {:spam, AntiSpam.service().check_user(email, current_ip, user_agent)}, @@ -176,6 +176,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do :registration_closed -> {:error, dgettext("errors", "Registrations are not open")} + :moderation_empty -> + {:error, dgettext("errors", "Moderation text must not be empty")} + :not_allowlisted -> {:error, dgettext("errors", "Your email is not on the allowlist")} @@ -198,12 +201,12 @@ defmodule Mobilizon.GraphQL.Resolvers.User do end end - @spec check_registration_config(String.t()) :: - :registration_ok | :registration_closed | :not_allowlisted - defp check_registration_config(email) do + @spec check_registration_config(String.t(), String.t()) :: + :registration_ok | :registration_closed | :not_allowlisted | :moderation_empty + defp check_registration_config(email, moderation) do cond do Config.instance_registrations_open?() -> - :registration_ok + check_moderation(moderation) Config.instance_registrations_allowlist?() -> check_allow_listed_email(email) @@ -223,6 +226,19 @@ defmodule Mobilizon.GraphQL.Resolvers.User do else: :not_deny_listed end + @spec check_moderation(String.t()) :: :registration_ok | :moderation_empty + defp check_moderation(moderation) do + if Config.instance_registrations_moderation?() do + if moderation |> String.trim() == "" do + :moderation_empty + else + :registration_ok + end + else + :registration_ok + end + 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()), diff --git a/lib/graphql/schema/user.ex b/lib/graphql/schema/user.ex index 91936b659..a959f1212 100644 --- a/lib/graphql/schema/user.ex +++ b/lib/graphql/schema/user.ex @@ -25,6 +25,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do interfaces([:action_log_object]) field(:id, :id, description: "The user's ID") field(:email, non_null(:string), description: "The user's email") + field(:moderation, :string, description: "The user's moderation text") field(:actors, non_null(list_of(:person)), description: "The user's list of profiles (identities)" @@ -345,6 +346,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do field :create_user, type: :user do arg(:email, non_null(:string), description: "The new user's email") arg(:password, non_null(:string), description: "The new user's password") + arg(:moderation, :string, description: "The new user's moderation text") arg(:locale, :string, description: "The new user's locale") middleware(Rajska.QueryAuthorization, permit: :all) middleware(Rajska.RateLimiter, limit: user_ip_limiter(@env)) diff --git a/lib/mobilizon/users/user.ex b/lib/mobilizon/users/user.ex index a0ab59673..62319cfe4 100644 --- a/lib/mobilizon/users/user.ex +++ b/lib/mobilizon/users/user.ex @@ -16,6 +16,7 @@ defmodule Mobilizon.Users.User do @type t :: %__MODULE__{ email: String.t(), + moderation: String.t(), password_hash: String.t(), password: String.t(), role: atom(), @@ -40,6 +41,7 @@ defmodule Mobilizon.Users.User do @required_attrs [:email] @optional_attrs [ + :moderation, :role, :password, :password_hash, @@ -61,7 +63,6 @@ defmodule Mobilizon.Users.User do @attrs @required_attrs ++ @optional_attrs @registration_required_attrs @required_attrs ++ [:password] - @auth_provider_required_attrs @required_attrs ++ [:provider] @password_change_required_attrs [:password] @@ -72,6 +73,7 @@ defmodule Mobilizon.Users.User do schema "users" do field(:email, :string) + field(:moderation, :string) field(:password_hash, :string) field(:password, :string, virtual: true) field(:role, UserRole, default: :user) diff --git a/priv/repo/migrations/20250911120910_add_moderation_to_users.exs b/priv/repo/migrations/20250911120910_add_moderation_to_users.exs new file mode 100644 index 000000000..b1785b718 --- /dev/null +++ b/priv/repo/migrations/20250911120910_add_moderation_to_users.exs @@ -0,0 +1,9 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddModerationToUser do + use Ecto.Migration + + def change do + alter table(:users) do + add(:moderation, :string, default: "") + end + end +end diff --git a/schema.graphql b/schema.graphql index c59b1bd1d..0c6395622 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1263,6 +1263,9 @@ type RootMutationType { "The new user's password" password: String! + "The new user's moderation text" + moderation: String! + "The new user's locale" locale: String ): User diff --git a/test/graphql/resolvers/user_test.exs b/test/graphql/resolvers/user_test.exs index 3c2c425bd..7e32d0a40 100644 --- a/test/graphql/resolvers/user_test.exs +++ b/test/graphql/resolvers/user_test.exs @@ -60,10 +60,11 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do """ @create_user_mutation """ - mutation CreateUser($email: String!, $password: String!, $locale: String) { + mutation CreateUser($email: String!, $password: String!, $moderation: String!, $locale: String) { createUser( email: $email password: $password + moderation: $moderation locale: $locale ) { id, @@ -133,7 +134,12 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do } """ - @valid_actor_params %{email: "test@test.tld", password: "testest", username: "test"} + @valid_actor_params %{ + email: "test@test.tld", + password: "testest", + moderation: "", + username: "test" + } @valid_single_actor_params %{preferred_username: "test2", keys: "yolo"} describe "Resolver: Get an user" do @@ -355,17 +361,31 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do email: "test@demo.tld", password: "long password", locale: "fr_FR", + moderation: " ", + preferredUsername: "toto", + name: "Sir Toto", + summary: "Sir Toto, prince of the functional tests" + } + @user_creation_with_moderation %{ + email: "test@demo.tld", + password: "long password", + locale: "fr_FR", + moderation: "moderation text", preferredUsername: "toto", name: "Sir Toto", summary: "Sir Toto, prince of the functional tests" } @user_creation_bad_email %{ email: "y@l@", - password: "long password" + password: "long password", + moderation: "" } test "test create_user/3 creates an user", %{conn: conn} do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], false) + res = conn |> AbsintheHelpers.graphql_query( @@ -382,6 +402,9 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "create_user/3 doesn't allow two users with the same email", %{conn: conn} do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], false) + res = conn |> put_req_header("accept-language", "fr") @@ -421,6 +444,44 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do Config.put([:instance, :registrations_moderation], false) end + test "create_user/3 allows registration with moderation text empty", %{conn: conn} do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], true) + + res = + conn + |> AbsintheHelpers.graphql_query( + query: @create_user_mutation, + variables: @user_creation + ) + + assert hd(res["errors"])["message"] == "Moderation text must not be empty" + + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], false) + end + + test "create_user/3 allows registration with moderation text", %{conn: conn} do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], true) + + res = + conn + |> AbsintheHelpers.graphql_query( + query: @create_user_mutation, + variables: @user_creation_with_moderation + ) + + assert res["data"]["createUser"]["email"] == @user_creation.email + assert res["data"]["createUser"]["locale"] == @user_creation.locale + + {:ok, user} = Users.get_user_by_email(@user_creation.email) + assert user.moderation == @user_creation_with_moderation.moderation + + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], false) + end + test "create_user/3 doesn't allow registration when user email is not on the allowlist", %{ conn: conn } do @@ -484,6 +545,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do test "create_user/3 doesn't allow registration when user email domain is on the denylist", %{ conn: conn } do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], false) Config.put([:instance, :registration_email_denylist], ["demo.tld"]) res = @@ -504,6 +567,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do test "create_user/3 doesn't allow registration when user email is on the denylist", %{ conn: conn } do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], false) Config.put([:instance, :registration_email_denylist], [@user_creation.email]) res = @@ -525,6 +590,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do %{ conn: conn } do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], false) Config.put([:instance, :registration_email_denylist], [@user_creation.email]) res = @@ -546,6 +613,9 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do %{ conn: conn } do + Config.put([:instance, :registrations_open], true) + Config.put([:instance, :registrations_moderation], false) + res = conn |> AbsintheHelpers.graphql_query( @@ -621,7 +691,12 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do describe "Resolver: Resend confirmation emails" do test "test resend_confirmation_email/3 with valid email resends an validation email", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"}) + {:ok, %User{} = user} = + Users.register(%{ + email: "toto@tata.tld", + password: "p4ssw0rd", + moderation: @moderation_empty + }) res = AbsintheHelpers.graphql_query(conn, @@ -714,7 +789,11 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do test "test send_reset_password/3 for a deactivated user doesn't send email", %{conn: conn} do {:ok, %User{email: email} = user} = - Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"}) + Users.register(%{ + email: "toto@tata.tld", + password: "p4ssw0rd", + moderation: @moderation_empty + }) Users.update_user(user, %{confirmed_at: DateTime.utc_now(), disabled: true}) @@ -732,7 +811,13 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do describe "Resolver: Reset user's password" do test "test reset_password/3 with valid email", context do - {:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"}) + {:ok, %User{} = user} = + Users.register(%{ + email: "toto@tata.tld", + password: "p4ssw0rd", + moderation: @moderation_empty + }) + Users.update_user(user, %{confirmed_at: DateTime.utc_now()}) %Actor{} = insert(:actor, user: user) {:ok, _email_sent} = Email.User.send_password_reset_email(user) @@ -814,7 +899,12 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do describe "Resolver: Login a user" do test "test login_user/3 with valid credentials", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"}) + {:ok, %User{} = user} = + Users.register(%{ + email: "toto@tata.tld", + password: "p4ssw0rd", + moderation: @moderation_empty + }) {:ok, %User{} = _user} = Users.update_user(user, %{ @@ -835,7 +925,12 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "test login_user/3 with invalid password", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"}) + {:ok, %User{} = user} = + Users.register(%{ + email: "toto@tata.tld", + password: "p4ssw0rd", + moderation: @moderation_empty + }) {:ok, %User{} = _user} = Users.update_user(user, %{ @@ -868,7 +963,12 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "test login_user/3 with unconfirmed user", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"}) + {:ok, %User{} = user} = + Users.register(%{ + email: "toto@tata.tld", + password: "p4ssw0rd", + moderation: @moderation_empty + }) res = conn @@ -990,11 +1090,14 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do describe "Resolver: Change password for an user" do @email "toto@tata.tld" + @moderation_fill "moderation text" + @moderation_empty "" @old_password "p4ssw0rd" @new_password "upd4t3d" test "change_password/3 with valid password", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @old_password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @old_password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1066,7 +1169,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "change_password/3 with invalid password", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @old_password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @old_password, moderation: @moderation_empty}) # Hammer time ! @@ -1094,7 +1198,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "change_password/3 with same password", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @old_password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @old_password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1122,7 +1227,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "change_password/3 with new password too short", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @old_password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @old_password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1150,7 +1256,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "change_password/3 without being authenticated", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @old_password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @old_password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1183,7 +1290,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do @password "p4ssw0rd" test "change_email/3 with valid email", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @old_email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @old_email, password: @password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1233,7 +1341,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "change_email/3 with valid email but invalid token", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @old_email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @old_email, password: @password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1286,7 +1395,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "change_email/3 with invalid password", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @old_email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @old_email, password: @password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1308,7 +1418,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "change_email/3 with same email", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @old_email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @old_email, password: @password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1330,7 +1441,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "change_email/3 with invalid email", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @old_email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @old_email, password: @password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1352,7 +1464,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "change_password/3 without being authenticated", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @old_email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @old_email, password: @password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = @@ -1379,7 +1492,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do @password "p4ssw0rd" test "delete_account/3 with valid password", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = user} = @@ -1453,7 +1567,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "delete_account/3 with invalid password", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = user} = @@ -1475,7 +1590,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do end test "delete_account/3 without being authenticated", %{conn: conn} do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @password, moderation: @moderation_empty}) # Hammer time ! {:ok, %User{} = _user} = diff --git a/test/mobilizon/users/users_test.exs b/test/mobilizon/users/users_test.exs index 28f6a87f8..0c916bc5e 100644 --- a/test/mobilizon/users/users_test.exs +++ b/test/mobilizon/users/users_test.exs @@ -7,9 +7,9 @@ defmodule Mobilizon.UsersTest do import Mobilizon.Factory describe "users" do - @valid_attrs %{email: "foo@bar.tld", password: "some password"} + @valid_attrs %{email: "foo@bar.tld", password: "some password", moderation: "moderation text"} @update_attrs %{email: "foo@fighters.tld", password: "some updated password"} - @invalid_attrs %{email: nil, password: nil} + @invalid_attrs %{email: nil, password: nil, moderation: nil} test "list_users/0 returns all users" do user = insert(:user) @@ -69,9 +69,11 @@ defmodule Mobilizon.UsersTest do @email "email@domain.tld" @password "password" + @moderation "moderation text" test "get_user_by_email/1 finds an user by its email" do - {:ok, %User{email: email} = user} = Users.register(%{email: @email, password: @password}) + {:ok, %User{email: email} = user} = + Users.register(%{email: @email, password: @password, moderation: @moderation}) assert email == @email {:ok, %User{id: id}} = Users.get_user_by_email(@email) @@ -80,7 +82,8 @@ defmodule Mobilizon.UsersTest do end test "get_user_by_email/1 finds an activated user by its email" do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @password, moderation: @moderation}) {:ok, %User{id: id}} = Users.get_user_by_email(@email, activated: false) assert id == user.id @@ -99,7 +102,8 @@ defmodule Mobilizon.UsersTest do @unconfirmed_email "unconfirmed@email.com" test "get_user_by_email/1 finds an user by its pending email" do - {:ok, %User{} = user} = Users.register(%{email: @email, password: @password}) + {:ok, %User{} = user} = + Users.register(%{email: @email, password: @password, moderation: @moderation}) Users.update_user(user, %{ "confirmed_at" => DateTime.utc_now() |> DateTime.truncate(:second), diff --git a/test/support/factory.ex b/test/support/factory.ex index c4a784411..617451f12 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -16,6 +16,7 @@ defmodule Mobilizon.Factory do %Mobilizon.Users.User{ password_hash: "Jane Smith", email: sequence(:email, &"email-#{&1}@example.com"), + moderation: "", role: :user, confirmed_at: DateTime.utc_now() |> DateTime.truncate(:second), confirmation_sent_at: nil,