diff --git a/.env.production.sample b/.env.production.sample new file mode 100644 index 000000000..d9a46ee8a --- /dev/null +++ b/.env.production.sample @@ -0,0 +1,22 @@ +# Settings +MOBILIZON_INSTANCE_NAME="<%= instance_name %>" +MOBILIZON_INSTANCE_HOST="<%= instance_domain %>" +MOBILIZON_INSTANCE_EMAIL="<%= instance_email %>" +MOBILIZON_INSTANCE_REGISTRATIONS_OPEN=true + +# API +GRAPHQL_API_ENDPOINT="https://<%= instance_domain %>" +GRAPHQL_API_FULL_PATH="" + +# APP +MIX_ENV=prod +PORT=4002 +MOBILIZON_LOGLEVEL="info" +MOBILIZON_SECRET="<%= instance_secret %>" + +# Database +MOBILIZON_DATABASE_USERNAME="mobilizon" +MOBILIZON_DATABASE_PASSWORD="<%= database_password %>" +MOBILIZON_DATABASE_DBNAME="mobilizon_prod" +MOBILIZON_DATABASE_HOST="localhost" +MOBILIZON_DATABASE_PORT=5432 \ No newline at end of file diff --git a/.gitignore b/.gitignore index d723bace9..eb03b60eb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ erl_crash.dump # variables. /config/*.secret.exs +.env.production + +setup_db.psql + .elixir_ls /doc priv/static/* diff --git a/config/config.exs b/config/config.exs index 0b63f9ebe..75ba5c4c0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -63,7 +63,4 @@ config :geolix, config :arc, storage: Arc.Storage.Local -config :email_checker, - validations: [EmailChecker.Check.Format] - config :phoenix, :format_encoders, json: Jason diff --git a/config/prod.exs b/config/prod.exs index 1033fcdce..d5da91522 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -19,8 +19,20 @@ config :mobilizon, MobilizonWeb.Endpoint, host: System.get_env("MOBILIZON_HOST") || "example.com", port: 80 ], + secret_key_base: + System.get_env("MOBILIZON_SECRET") || "ThisShouldBeAVeryStrongStringPleaseReplaceMe", cache_static_manifest: "priv/static/cache_manifest.json" +# Configure your database +config :mobilison, Mobilizon.Repo, + adapter: Ecto.Adapters.Postgres, + username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon", + password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon", + database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_prod", + hostname: System.get_env("MOBILIZON_DATABASE_HOST") || "localhost", + port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432", + pool_size: 15 + config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.SMTPAdapter, server: "localhost", @@ -80,7 +92,3 @@ config :logger, level: System.get_env("MOBILIZON_LOGLEVEL") |> String.to_atom() # # config :mobilizon, MobilizonWeb.Endpoint, server: true # - -# Finally import the config/prod.secret.exs -# which should be versioned separately. -import_config "prod.secret.exs" diff --git a/js/.env.dist b/js/.env.dist deleted file mode 100644 index fc4c3f76f..000000000 --- a/js/.env.dist +++ /dev/null @@ -1,3 +0,0 @@ -API_HOST=mobilizon.tld -API_ORIGIN=https://mobilizon.tld -API_PATH=/api/v1 diff --git a/js/src/api/_entrypoint.js b/js/src/api/_entrypoint.js index 221d7161b..ff2558d33 100644 --- a/js/src/api/_entrypoint.js +++ b/js/src/api/_entrypoint.js @@ -1,3 +1,26 @@ -export const API_HOST = process.env.API_HOST; -export const API_ORIGIN = process.env.API_ORIGIN; -export const API_PATH = process.env.API_PATH; +/** + * Host of the instance + * + * Required + * + * Example: framameet.org + */ +export const MOBILIZON_INSTANCE_HOST = process.env.MOBILIZON_INSTANCE_HOST; + +/** + * URL on which the API is. "/api" will be added at the end + * + * Required + * + * Example: https://framameet.org + */ +export const GRAPHQL_API_ENDPOINT = process.env.GRAPHQL_API_ENDPOINT; + +/** + * URL with path on which the API is. Replaces GRAPHQL_API_ENDPOINT if used + * + * Optional + * + * Example: https://framameet.org/api + */ +export const GRAPHQL_API_FULL_PATH = process.env.GRAPHQL_API_FULL_PATH; diff --git a/js/src/api/osm.js b/js/src/api/osm.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/js/src/vue-apollo.js b/js/src/vue-apollo.js index 7caf0a69b..5e8cbf76c 100644 --- a/js/src/vue-apollo.js +++ b/js/src/vue-apollo.js @@ -5,12 +5,14 @@ import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemo import { createLink } from 'apollo-absinthe-upload-link'; import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'; import { AUTH_TOKEN } from './constants'; +import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint'; // Install the vue plugin Vue.use(VueApollo); // Http endpoint -const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/api'; +const httpServer = GRAPHQL_API_ENDPOINT || 'http://localhost:4000'; +const httpEndpoint = GRAPHQL_API_FULL_PATH || `${httpServer}/api`; const fragmentMatcher = new IntrospectionFragmentMatcher({ introspectionQueryResultData: { diff --git a/lib/mix/tasks/generate_config.ex b/lib/mix/tasks/generate_config.ex new file mode 100644 index 000000000..acb001787 --- /dev/null +++ b/lib/mix/tasks/generate_config.ex @@ -0,0 +1,121 @@ +defmodule Mix.Tasks.GenerateConfig do + use Mix.Task + + @moduledoc """ + Generate a new config + + ## Usage + ``mix generate_config`` + + This mix task is interactive, and will overwrite the environment file present at ``.env.production``. + + Inspired from Pleroma own generate_config task + """ + def run(_) do + IO.puts("Answer a few questions to generate a new config\n") + + override = + if File.exists?(".env.production") do + confirm("You already have an .env.production file, do you want to override it?") + else + nil + end + + if override == true do + IO.puts("\n--- THIS WILL OVERWRITE YOUR .env.production file! ---\n") + end + + if override != false do + domain = string_required("What is your domain name? (e.g. framameet.org): ") + name = string_required("What is the name of your instance? (e.g. Framameet): ") + email = email("What's your admin email address: ") + + if confirm("Is everything okay?") do + do_generate(domain, name, email) + else + IO.puts("\nYou cancelled installation\n") + end + else + IO.puts("\nYou cancelled installation\n") + end + end + + defp do_generate(domain, name, email) do + secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) + + # Try to avoid issues with some special caracters using url_encode64() + dbpass = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64) + + resultSql = EEx.eval_file("support/postgresql/setup_db.psql", database_password: dbpass) + + result = + EEx.eval_file( + ".env.production.sample", + instance_domain: domain, + instance_name: name, + instance_email: email, + instance_secret: secret, + database_password: dbpass + ) + + IO.puts("\nWriting config to .env.production.\n\nCheck it and configure your database.") + + File.write(".env.production", result) + + IO.puts(""" + \nWriting setup_db.psql, please run it as postgres superuser, i.e.: sudo su postgres -c 'psql -f setup_db.psql'\n + You may delete the setup_db.psql file once it has been executed. + """) + + File.write("setup_db.psql", resultSql) + end + + # Taken from ex_prompt + @spec confirm(String.t()) :: boolean() + defp confirm(prompt) do + answer = + String.trim(prompt) + |> Kernel.<>(" [Yn] ") + |> string() + |> String.downcase() + + cond do + answer in ~w(yes y) -> true + answer in ~w(no n) -> false + true -> confirm(prompt) + end + end + + # Taken from ex_prompt + @spec string(String.t()) :: String.t() + defp string(prompt) do + case IO.gets(prompt) do + :eof -> "" + {:error, _reason} -> "" + str -> String.trim_trailing(str) + end + end + + # Taken from ex_prompt + @spec string_required(String.t()) :: String.t() + defp string_required(prompt) do + case string(prompt) do + "" -> string_required(prompt) + str -> str + end + end + + @spec email(String.t(), boolean()) :: String.t() + defp email(prompt, required \\ true) do + email_value = + case required do + true -> string_required(prompt) + _ -> string(prompt) + end + + case Mobilizon.Service.EmailChecker.valid?(email_value) do + false -> email(prompt, required) + _ -> email_value + end + end +end diff --git a/lib/mobilizon/actors/user.ex b/lib/mobilizon/actors/user.ex index 1253108e5..aa15f9a69 100644 --- a/lib/mobilizon/actors/user.ex +++ b/lib/mobilizon/actors/user.ex @@ -5,6 +5,7 @@ defmodule Mobilizon.Actors.User do use Ecto.Schema import Ecto.Changeset alias Mobilizon.Actors.{Actor, User} + alias Mobilizon.Service.EmailChecker schema "users" do field(:email, :string) diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex index 9e39bf92e..be2a9c794 100644 --- a/lib/mobilizon_web/schema.ex +++ b/lib/mobilizon_web/schema.ex @@ -27,12 +27,19 @@ defmodule MobilizonWeb.Schema do field(:summary, :string, description: "The actor's summary") field(:preferred_username, :string, description: "The actor's preferred username") field(:keys, :string, description: "The actors RSA Keys") - field(:manually_approves_followers, :boolean, description: "Whether the actors manually approves followers") + + field(:manually_approves_followers, :boolean, + description: "Whether the actors manually approves followers" + ) + field(:suspended, :boolean, description: "If the actor is suspended") field(:avatar_url, :string, description: "The actor's avatar url") field(:banner_url, :string, description: "The actor's banner url") # field(:followers, list_of(:follower)) - field(:organized_events, list_of(:event), resolve: dataloader(Events), description: "A list of the events this actor has organized") + field(:organized_events, list_of(:event), + resolve: dataloader(Events), + description: "A list of the events this actor has organized" + ) # field(:memberships, list_of(:member)) field(:user, :user, description: "The user this actor is associated to") @@ -52,13 +59,29 @@ defmodule MobilizonWeb.Schema do field(:id, non_null(:id), description: "The user's ID") field(:email, non_null(:string), description: "The user's email") # , resolve: dataloader(:actors)) - field(:actors, non_null(list_of(:actor)), description: "The user's list of actors (identities)") + field(:actors, non_null(list_of(:actor)), + description: "The user's list of actors (identities)" + ) + field(:default_actor_id, non_null(:integer), description: "The user's default actor") - field(:confirmed_at, :datetime, description: "The datetime when the user was confirmed/activated") - field(:confirmation_sent_at, :datetime, description: "The datetime the last activation/confirmation token was sent") + + field(:confirmed_at, :datetime, + description: "The datetime when the user was confirmed/activated" + ) + + field(:confirmation_sent_at, :datetime, + description: "The datetime the last activation/confirmation token was sent" + ) + field(:confirmation_token, :string, description: "The account activation/confirmation token") - field(:reset_password_sent_at, :datetime, description: "The datetime last reset password email was sent") - field(:reset_password_token, :string, description: "The token sent when requesting password token") + + field(:reset_password_sent_at, :datetime, + description: "The datetime last reset password email was sent" + ) + + field(:reset_password_token, :string, + description: "The token sent when requesting password token" + ) end @desc "A JWT and the associated user ID" diff --git a/lib/service/email_checker.ex b/lib/service/email_checker.ex new file mode 100644 index 000000000..62cbe7efa --- /dev/null +++ b/lib/service/email_checker.ex @@ -0,0 +1,15 @@ +defmodule Mobilizon.Service.EmailChecker do + @moduledoc """ + Provides a function to test emails against a "not so bad" regex + """ + + @email_regex ~r/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + + @doc """ + Returns whether the email is valid + """ + @spec valid?(String.t()) :: boolean() + def valid?(email) do + email =~ @email_regex + end +end diff --git a/mix.exs b/mix.exs index c5ecf3db7..7f3719a40 100644 --- a/mix.exs +++ b/mix.exs @@ -87,7 +87,6 @@ defmodule Mobilizon.Mixfile do {:dataloader, "~> 1.0"}, {:arc, "~> 0.11.0"}, {:arc_ecto, "~> 0.11.0"}, - {:email_checker, "~> 0.1.2"}, {:plug_cowboy, "~> 1.0"}, # Dev and test dependencies {:phoenix_live_reload, "~> 1.0", only: :dev}, diff --git a/support/systemd/mobilizon.service b/support/systemd/mobilizon.service new file mode 100644 index 000000000..b082ba514 --- /dev/null +++ b/support/systemd/mobilizon.service @@ -0,0 +1,27 @@ +[Unit] +Description=Mobilizon Service +After=network.target postgresql.service + +[Service] +User=mobilizon +WorkingDirectory=/home/mobilizon/mobilizon +ExecStart=/usr/local/bin/mix phx.server +ExecReload=/bin/kill $MAINPID +KillMode=process +Restart=on-failure +EnvironmentFile=/var/www/mobilizon/.env + +; Some security directives. +; Use private /tmp and /var/tmp folders inside a new file system namespace, which are discarded after the process stops. +PrivateTmp=true +; Mount /usr, /boot, and /etc as read-only for processes invoked by this service. +ProtectSystem=full +; Sets up a new /dev mount for the process and only adds API pseudo devices like /dev/null, /dev/zero or /dev/random but not physical devices. Disabled by default because it may not work on devices like the Raspberry Pi. +PrivateDevices=false +; Ensures that the service process and all its children can never gain new privileges through execve(). +NoNewPrivileges=true + + +[Install] +WantedBy=multi-user.target +Alias=mobilizon.service