diff --git a/config/config.exs b/config/config.exs index f77402a..c75f9ce 100644 --- a/config/config.exs +++ b/config/config.exs @@ -18,7 +18,8 @@ config :lokal, LokalWeb.Endpoint, secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I", render_errors: [view: LokalWeb.ErrorView, accepts: ~w(html json), layout: false], pubsub_server: Lokal.PubSub, - live_view: [signing_salt: "zOLgd3lr"] + live_view: [signing_salt: "zOLgd3lr"], + registration: System.get_env("REGISTRATION") || "invite" config :lokal, Lokal.Application, automigrate: false @@ -39,6 +40,15 @@ config :lokal, Lokal.Mailer, adapter: Swoosh.Adapters.Local # Swoosh API client is needed for adapters other than SMTP. config :swoosh, :api_client, false +# Gettext +config :gettext, :default_locale, "en_US" + +# Configure Oban +config :lokal, Oban, + repo: Lokal.Repo, + plugins: [Oban.Plugins.Pruner], + queues: [default: 10, mailers: 20] + # Configure esbuild (the version is required) # config :esbuild, # version: "0.14.0", diff --git a/config/dev.exs b/config/dev.exs index 3b8a5d8..e454a92 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -2,9 +2,6 @@ import Config # Configure your database config :lokal, Lokal.Repo, - url: - System.get_env("DATABASE_URL") || - "ecto://postgres:postgres@localhost/lokal_dev", show_sensitive_data_on_connection_error: true, pool_size: 10 @@ -15,13 +12,10 @@ config :lokal, Lokal.Repo, # watchers to your application. For example, we use it # with esbuild to bundle .js and .css sources. config :lokal, LokalWeb.Endpoint, - # Binding to loopback ipv4 address prevents access from other machines. - # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. - http: [ip: {0, 0, 0, 0}, port: 4000], check_origin: false, code_reloader: true, debug_errors: true, - secret_key_base: "cSLRa17z1D1qLwQuaw73DMT7BX8oDMkru/rJIsmCdlFypLGRQW3bpqJRrZQtoZJQ", + secret_key_base: "dg2lccMgaY3+ZeKppR+ondk4ZRaANZGIN0LMZT1u1uzscH4jO5W9a9b9V9BkC+MW", watchers: [ # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args) # esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} diff --git a/config/prod.exs b/config/prod.exs index 8b6f640..6bdebd1 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -9,16 +9,7 @@ import Config # manifest is generated by the `mix phx.digest` task, # which you should run after static files are built and # before starting your production server. -config :lokal, LokalWeb.Endpoint, - url: [host: "localhost"], - http: [port: 4000], - cache_static_manifest: "priv/static/cache_manifest.json" - -config :lokal, Lokal.Repo, - url: "ecto://postgres:postgres@localhost/lokal", - pool_size: 10 - -config :lokal, Lokal.Application, automigrate: true +config :lokal, LokalWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" # Do not print debug messages in production config :logger, level: :info diff --git a/config/runtime.exs b/config/runtime.exs index e2e6c53..3fbb035 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -12,19 +12,49 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do config :lokal, LokalWeb.Endpoint, server: true end -if config_env() == :prod do - database_url = +config :lokal, LokalWeb.ViewHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true" + +# Set locale +Gettext.put_locale(System.get_env("LOCALE") || "en_US") + +maybe_ipv6 = if System.get_env("ECTO_IPV6") == "true", do: [:inet6], else: [] + +database_url = + if config_env() == :test do + System.get_env("TEST_DATABASE_URL") || + "ecto://postgres:postgres@localhost/lokal_test#{System.get_env("MIX_TEST_PARTITION")}" + else System.get_env("DATABASE_URL") || "ecto://postgres:postgres@lokal-db/lokal" + end - maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: [] +host = + System.get_env("HOST") || + raise "No hostname set! Must be the domain and tld like `lokal.bubbletea.dev`." - config :lokal, Lokal.Repo, - # ssl: true, - url: database_url, - pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), - socket_options: maybe_ipv6 +interface = + if config_env() in [:dev, :test], + do: {0, 0, 0, 0}, + else: {0, 0, 0, 0, 0, 0, 0, 0} +config :lokal, Lokal.Repo, + # ssl: true, + url: database_url, + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), + socket_options: maybe_ipv6 + +config :lokal, LokalWeb.Endpoint, + url: [scheme: "https", host: host, port: 443], + http: [ + # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html + # for details about using IPv6 vs IPv4 and loopback vs public addresses. + ip: interface, + port: String.to_integer(System.get_env("PORT") || "4000") + ], + server: true, + registration: System.get_env("REGISTRATION") || "invite" + +if config_env() == :prod do # The secret key base is used to sign/encrypt cookies and other secrets. # A default value is used in config/dev.exs and config/test.exs but you # want to use a different value for prod and you most likely don't want @@ -37,20 +67,21 @@ if config_env() == :prod do You can generate one by calling: mix phx.gen.secret """ - host = System.get_env("HOST") || "localhost" + config :lokal, LokalWeb.Endpoint, secret_key_base: secret_key_base - config :lokal, LokalWeb.Endpoint, - ururl: [scheme: "https", host: host, port: 443], - http: [ - # Enable IPv6 and bind on all interfaces. - # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. - # See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html - # for details about using IPv6 vs IPv4 and loopback vs public addresses. - ip: {0, 0, 0, 0, 0, 0, 0, 0}, - port: String.to_integer(System.get_env("PORT") || "4000") - ], - secret_key_base: secret_key_base, - server: true + # Automatically apply migrations + config :lokal, Lokal.Application, automigrate: true + + # Set up SMTP settings + config :lokal, Lokal.Mailer, + adapter: Swoosh.Adapters.SMTP, + relay: System.get_env("SMTP_HOST") || raise("No SMTP_HOST set!"), + port: System.get_env("SMTP_PORT") || 587, + username: System.get_env("SMTP_USERNAME") || raise("No SMTP_USERNAME set!"), + password: System.get_env("SMTP_PASSWORD") || raise("No SMTP_PASSWORD set!"), + ssl: System.get_env("SMTP_SSL") == "true", + email_from: System.get_env("EMAIL_FROM") || "no-reply@#{System.get_env("HOST")}", + email_name: System.get_env("EMAIL_NAME") || "Lokal" # ## Using releases # @@ -61,22 +92,4 @@ if config_env() == :prod do # # Then you can assemble a release by calling `mix release`. # See `mix help release` for more information. - - # ## Configuring the mailer - # - # In production you need to configure the mailer to use a different adapter. - # Also, you may need to configure the Swoosh API client of your choice if you - # are not using SMTP. Here is an example of the configuration: - # - # config :lokal, Lokal.Mailer, - # adapter: Swoosh.Adapters.Mailgun, - # api_key: System.get_env("MAILGUN_API_KEY"), - # domain: System.get_env("MAILGUN_DOMAIN") - # - # For this example you need include a HTTP client required by Swoosh API client. - # Swoosh supports Hackney and Finch out of the box: - # - # config :swoosh, :api_client, Swoosh.ApiClient.Hackney - # - # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. end diff --git a/config/test.exs b/config/test.exs index 5835c36..d8fd1d2 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,14 +1,14 @@ import Config +# Only in tests, remove the complexity from the password hashing algorithm +config :bcrypt_elixir, :log_rounds, 1 + # Configure your database # # The MIX_TEST_PARTITION environment variable can be used # to provide built-in test partitioning in CI environment. # Run `mix help test` for more information. config :lokal, Lokal.Repo, - url: - System.get_env("TEST_DATABASE_URL") || - "ecto://postgres:postgres@localhost/lokal_test#{System.get_env("MIX_TEST_PARTITION")}", pool: Ecto.Adapters.SQL.Sandbox, pool_size: 10 @@ -16,7 +16,7 @@ config :lokal, Lokal.Repo, # you can enable the server option below. config :lokal, LokalWeb.Endpoint, http: [ip: {0, 0, 0, 0}, port: 4002], - secret_key_base: "T4DkRImgeMNCcPcTWBCZyKYp3KQ8yyPD33VT4wj6ogbP8fIGUsqmOTNX3clTMrLo", + secret_key_base: "S3qq9QtUdsFtlYej+HTjAVN95uP5i5tf2sPYINWSQfCKJghFj2B1+wTAoljZyHOK", server: false # In test we don't send emails. @@ -27,3 +27,6 @@ config :logger, level: :warn # Initialize plugs at runtime for faster test compilation config :phoenix, :plug_init_mode, :runtime + +# Disable Oban +config :lokal, Oban, queues: false, plugins: false diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..f8cb007 --- /dev/null +++ b/contributing.md @@ -0,0 +1,124 @@ +# Contribution Guide + +## Style Tips + +- In order to keep code concise and improve readability, please try to make your + functions as short as possible while keeping variable names descriptive! For + instance, use inline `do:` blocks for short functions and make your aliases as + short as possible without introducing ambiguity. + - I.e. since there's only one `Changeset` in the app, please alias + `Changeset.t(Type.t())` instead of using `Ecto.Changeset.t(Long.Type.t())` +- Use pipelines when possible. If only calling a single method, a pipeline isn't + strictly necessary but still encouraged for future modification. +- Please add typespecs to your functions! Even your private functions may be + used by others later down the line, and typespecs will be able to help + document your code just a little bit better, and improve the debugging + process. + - Typespec arguments can be named like `@spec function(arg_name :: type()) :: + return_type()`. Please use these for generic types, such as `map()` when the + input data isn't immediately obvious. + - Please define all typespecs for a function together in one place, instead of + each function header. +- When making new models, please take inspiration from the existing models in + regards to layout of sections, typespec design, and formatting. +- With Elixir convention, for methods that raise on error please name them like + `function_that_raises!()`, and functions that return a boolean like + `function_that_returns_boolean?()`. For other methods, it's encouraged to use + status tuples for other functions like `{:ok, result}` or `{:error, + reason_or_changeset}` instead of just returning `result` or `nil` for easy + pattern matching. +- Instead of using the `.` operator, try to use pattern matching instead, + especially for function headers. `.` in templates is fine to keep things + concise. +- Use `Enum` functions over comprehensions whenever possible for clarity. + However, comprehensions in templates are fine for legibility. +- When adding text, please use `gettext` macros to enable things to be + translated in the future. After adding `gettext` macros, run `mix format` in + order to add your new text strings to the files in `priv/gettext`. + - Existing domains: `"default"` (for anything general), `"prompts"` + (informational messages as a result of the user doing an action, i.e. in + flashes), `"actions"` (actions that the user can take), `"emails"`, and + `"errors"`. Using these domains accurately will let translators know which + translations are higher and lower priority. Thank you! +- Before submitting a PR, please make sure all tests are passing using `mix + test`. + +# Technical Information + +- Created using the [Phoenix Framework](https://www.phoenixframework.org) +- User Registration/Sign in via + [`phx_gen_auth`](https://hexdocs.pm/phx_gen_auth/). +- `Dockerfile` and example `docker-compose.yml` +- Automatic migrations in `MIX_ENV=prod` or Docker image +- JS linting with [standard.js](https://standardjs.com), HEEx linting with + [heex_formatter](https://github.com/feliperenan/heex_formatter) + +## Docs + +More information can be found in the documentation generated by `mix docs`. +These are located in the `/docs` folder, and are generated in HTML and ePub. +Check them out! + +# Instructions + +1. Clone the repo +1. Install the elixir and erlang binaries. I recommend using [asdf version + manager](https://asdf-vm.com/guide/getting-started.html#_1-install-dependencies), + which will use the `.tool-versions` file to install the correct versions of + Erlang, Elixir and npm for this project! +1. Run `mix deps.get` and `mix compile` to fetch all dependencies +1. Run `mix setup` to initialize your database. You can reset your database at + any time with `mix ecto.reset`. +1. Run migrations with `mix ecto.migrate` or rollback with `mix ecto.rollback`. +1. Run `mix phx.server` to start the development server. + +# Configuration + +For development, I recommend setting environment variables with +[direnv](https://direnv.net). + +By default, Lokal will always bind to all external IPv4 and IPv6 addresses in +`dev` and `prod` mode, respectively. If you would like to use different values, +they will need to be overridden in `config/dev.exs` and `config/runtime.exs` for +`dev` and `prod` modes, respectively. + +## `MIX_ENV=dev` + +In `dev` mode, Lokal will listen for these environment variables at runtime. + +- `HOST`: External url to generate links with. Set this especially if you're + behind a reverse proxy. Defaults to `localhost`. External URLs will always be + generated with `https://` and port `443`. +- `PORT`: Internal port to bind to. Defaults to `4000`. +- `DATABASE_URL`: Controls the database url to connect to. Defaults to + `ecto://postgres:postgres@localhost/lokal_dev`. +- `ECTO_IPV6`: Controls if Ecto should use IPv6 to connect to PostgreSQL. + Defaults to `false`. +- `POOL_SIZE`: Controls the pool size to use with PostgreSQL. Defaults to `10`. +- `REGISTRATION`: Controls if user sign-up should be invite only or set to public. Set to `public` to enable public registration. Defaults to `invite`. +- `LOCALE`: Sets a custom locale. Defaults to `en_US`. + +## `MIX_ENV=test` + +In `test` mode (or in the Docker container), Lokal will listen for the same environment variables as dev mode, but also include the following at runtime: + +- `TEST_DATABASE_URL`: REPLACES `DATABASE_URL`. Controls the database url to + connect to. Defaults to `ecto://postgres:postgres@localhost/lokal_test`. +- `MIX_TEST_PARTITION`: Only used if `TEST_DATABASE_URL` is not specified. + Appended to the default database url if you would like to partition your test + databases. Defaults to not set. + +## `MIX_ENV=prod` + +In `prod` mode (or in the Docker container), Lokal will listen for the same environment variables as dev mode, but also include the following at runtime: + +- `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated + with `docker run -it shibaobun/lokal mix phx.gen.secret` and set for server to start. +- `SMTP_HOST`: The url for your SMTP email provider. Must be set +- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`. +- `SMTP_USERNAME`: The username for your SMTP relay. Must be set! +- `SMTP_PASSWORD`: The password for your SMTP relay. Must be set! +- `SMTP_SSL`: Set to `true` to enable SSL for emails. Defaults to `false`. +- `EMAIL_FROM`: Sets the sender email in sent emails. Defaults to + `no-reply@HOST` where `HOST` was previously defined. +- `EMAIL_NAME`: Sets the sender name in sent emails. Defaults to "Lokal". diff --git a/lib/lokal/accounts/email.ex b/lib/lokal/accounts/email.ex new file mode 100644 index 0000000..af2c37d --- /dev/null +++ b/lib/lokal/accounts/email.ex @@ -0,0 +1,48 @@ +defmodule Lokal.Email do + @moduledoc """ + Emails that can be sent using Swoosh. + + You can find the base email templates at + `lib/Lokal_web/templates/layout/email.html.heex` for html emails and + `lib/Lokal_web/templates/layout/email.txt.heex` for text emails. + """ + + use Phoenix.Swoosh, view: LokalWeb.EmailView, layout: {LokalWeb.LayoutView, :email} + import LokalWeb.Gettext + alias Lokal.Accounts.User + alias LokalWeb.EmailView + + @typedoc """ + Represents an HTML and text body email that can be sent + """ + @type t() :: Swoosh.Email.t() + + @spec base_email(User.t(), String.t()) :: t() + defp base_email(%User{email: email}, subject) do + from = Application.get_env(:Lokal, Lokal.Mailer)[:email_from] || "noreply@localhost" + name = Application.get_env(:Lokal, Lokal.Mailer)[:email_name] + new() |> to(email) |> from({name, from}) |> subject(subject) + end + + @spec generate_email(key :: String.t(), User.t(), attrs :: map()) :: t() + def generate_email("welcome", user, %{"url" => url}) do + user + |> base_email(dgettext("emails", "Confirm your %{name} account", name: "Lokal")) + |> render_body("confirm_email.html", %{user: user, url: url}) + |> text_body(EmailView.render("confirm_email.txt", %{user: user, url: url})) + end + + def generate_email("reset_password", user, %{"url" => url}) do + user + |> base_email(dgettext("emails", "Reset your %{name} password", name: "Lokal")) + |> render_body("reset_password.html", %{user: user, url: url}) + |> text_body(EmailView.render("reset_password.txt", %{user: user, url: url})) + end + + def generate_email("update_email", user, %{"url" => url}) do + user + |> base_email(dgettext("emails", "Update your %{name} email", name: "Lokal")) + |> render_body("update_email.html", %{user: user, url: url}) + |> text_body(EmailView.render("update_email.txt", %{user: user, url: url})) + end +end diff --git a/lib/lokal/accounts/email_worker.ex b/lib/lokal/accounts/email_worker.ex new file mode 100644 index 0000000..fc94144 --- /dev/null +++ b/lib/lokal/accounts/email_worker.ex @@ -0,0 +1,13 @@ +defmodule Lokal.EmailWorker do + @moduledoc """ + Oban worker that dispatches emails + """ + + use Oban.Worker, queue: :mailers, tags: ["email"] + alias Lokal.{Accounts, Email, Mailer} + + @impl Oban.Worker + def perform(%Oban.Job{args: %{"email" => email, "user_id" => user_id, "attrs" => attrs}}) do + Email.generate_email(email, user_id |> Accounts.get_user!(), attrs) |> Mailer.deliver() + end +end diff --git a/lib/lokal/mailer.ex b/lib/lokal/mailer.ex index 51fd1dc..792696f 100644 --- a/lib/lokal/mailer.ex +++ b/lib/lokal/mailer.ex @@ -1,7 +1,42 @@ defmodule Lokal.Mailer do @moduledoc """ - Mailer, currently uses Swoosh + Mailer adapter for emails + + Since emails are loaded as Oban jobs, the `:attrs` map must be serializable to + json with Jason, which restricts the use of structs. """ use Swoosh.Mailer, otp_app: :lokal + alias Lokal.{Accounts.User, EmailWorker} + alias Oban.Job + + @doc """ + Deliver instructions to confirm account. + """ + @spec deliver_confirmation_instructions(User.t(), String.t()) :: Job.t() + def deliver_confirmation_instructions(%User{id: user_id}, url) do + %{email: :welcome, user_id: user_id, attrs: %{url: url}} + |> EmailWorker.new() + |> Oban.insert!() + end + + @doc """ + Deliver instructions to reset a user password. + """ + @spec deliver_reset_password_instructions(User.t(), String.t()) :: Job.t() + def deliver_reset_password_instructions(%User{id: user_id}, url) do + %{email: :reset_password, user_id: user_id, attrs: %{url: url}} + |> EmailWorker.new() + |> Oban.insert!() + end + + @doc """ + Deliver instructions to update a user email. + """ + @spec deliver_update_email_instructions(User.t(), String.t()) :: Job.t() + def deliver_update_email_instructions(%User{id: user_id}, url) do + %{email: :update_email, user_id: user_id, attrs: %{url: url}} + |> EmailWorker.new() + |> Oban.insert!() + end end diff --git a/lib/lokal_web/templates/email/confirm_email.html.eex b/lib/lokal_web/templates/email/confirm_email.html.eex new file mode 100644 index 0000000..99aee4e --- /dev/null +++ b/lib/lokal_web/templates/email/confirm_email.html.eex @@ -0,0 +1,25 @@ +
+ + <%= dgettext("emails", "Hi %{email},", email: @user.email) %> + + +
+ + + <%= dgettext("emails", "Welcome to %{name}!", name: "Lokal") %> + + +
+ + <%= dgettext("emails", "You can confirm your account by visiting the URL below:") %> + +
+ + <%= @url %> + +
+ + <%= dgettext("emails", + "If you didn't create an account at %{name}, please ignore this.", + name: "Lokal") %> +
diff --git a/lib/lokal_web/templates/email/confirm_email.txt.eex b/lib/lokal_web/templates/email/confirm_email.txt.eex new file mode 100644 index 0000000..ca7867d --- /dev/null +++ b/lib/lokal_web/templates/email/confirm_email.txt.eex @@ -0,0 +1,12 @@ + +<%= dgettext("emails", "Hi %{email},", email: @user.email) %> + +<%= dgettext("emails", "Welcome to %{name}%!", name: "Lokal") %> + +<%= dgettext("emails", "You can confirm your account by visiting the URL below:") %> + +<%= @url %> + +<%= dgettext("emails", + "If you didn't create an account at %{url}, please ignore this.", + url: Routes.live_url(Endpoint, PageLive)) %> diff --git a/lib/lokal_web/templates/email/reset_password.html.eex b/lib/lokal_web/templates/email/reset_password.html.eex new file mode 100644 index 0000000..7fe79bf --- /dev/null +++ b/lib/lokal_web/templates/email/reset_password.html.eex @@ -0,0 +1,19 @@ +
+ + <%= dgettext("emails", "Hi %{email},", email: @user.email) %> + + +
+ + <%= dgettext("emails", "You can reset your password by visiting the URL below:") %> + +
+ + <%= @url %> + +
+ + <%= dgettext("emails", + "If you didn't request this change from %{name}, please ignore this.", + name: "Lokal") %> +
diff --git a/lib/lokal_web/templates/email/reset_password.txt.eex b/lib/lokal_web/templates/email/reset_password.txt.eex new file mode 100644 index 0000000..69bdec9 --- /dev/null +++ b/lib/lokal_web/templates/email/reset_password.txt.eex @@ -0,0 +1,10 @@ + +<%= dgettext("emails", "Hi %{email},", email: @user.email) %> + +<%= dgettext("emails", "You can reset your password by visiting the URL below:") %> + +<%= @url %> + +<%= dgettext("emails", + "If you didn't request this change from %{url}, please ignore this.", + url: Routes.live_url(Endpoint, PageLive)) %> diff --git a/lib/lokal_web/templates/email/update_email.html.eex b/lib/lokal_web/templates/email/update_email.html.eex new file mode 100644 index 0000000..d51e084 --- /dev/null +++ b/lib/lokal_web/templates/email/update_email.html.eex @@ -0,0 +1,19 @@ +
+ + <%= dgettext("emails", "Hi %{email},", email: @user.email) %> + + +
+ + <%= dgettext("emails", "You can change your email by visiting the URL below:") %> + +
+ + <%= @url %> + +
+ + <%= dgettext("emails", + "If you didn't request this change from %{name}, please ignore this.", + name: "Lokal") %> +
diff --git a/lib/lokal_web/templates/email/update_email.txt.eex b/lib/lokal_web/templates/email/update_email.txt.eex new file mode 100644 index 0000000..30d44f4 --- /dev/null +++ b/lib/lokal_web/templates/email/update_email.txt.eex @@ -0,0 +1,10 @@ + +<%= dgettext("emails", "Hi %{email},", email: @user.email) %> + +<%= dgettext("emails", "You can change your email by visiting the URL below:") %> + +<%= @url %> + +<%= dgettext("emails", + "If you didn't request this change from %{url}, please ignore this.", + url: Routes.live_url(Endpoint, PageLive)) %> diff --git a/lib/lokal_web/templates/layout/email.html.heex b/lib/lokal_web/templates/layout/email.html.heex new file mode 100644 index 0000000..c8622e4 --- /dev/null +++ b/lib/lokal_web/templates/layout/email.html.heex @@ -0,0 +1,24 @@ + + + + <%= @email.subject %> + + + + <%= @inner_content %> + +
+ + + <%= dgettext( + "emails", + "This email was sent from %{name}, the self-hosted firearm tracker website.", + name: "Lokal" + ) %> + + + diff --git a/lib/lokal_web/templates/layout/email.txt.eex b/lib/lokal_web/templates/layout/email.txt.eex new file mode 100644 index 0000000..09791c7 --- /dev/null +++ b/lib/lokal_web/templates/layout/email.txt.eex @@ -0,0 +1,12 @@ +<%= @email.subject %> + +==================== + +<%= @inner_content %> + +===================== + +<%= dgettext("emails", + "This email was sent from %{name} at %{url}, the self-hosted firearm tracker website.", + name: "Lokal", + url: Routes.live_url(Endpoint, PageLive)) %> diff --git a/lib/lokal_web/templates/layout/empty.html.heex b/lib/lokal_web/templates/layout/empty.html.heex new file mode 100644 index 0000000..0543398 --- /dev/null +++ b/lib/lokal_web/templates/layout/empty.html.heex @@ -0,0 +1 @@ +<%= @inner_content %> diff --git a/lib/lokal_web/views/email_view.ex b/lib/lokal_web/views/email_view.ex new file mode 100644 index 0000000..f81184d --- /dev/null +++ b/lib/lokal_web/views/email_view.ex @@ -0,0 +1,8 @@ +defmodule LokalWeb.EmailView do + @moduledoc """ + A view for email-related helper functions + """ + alias LokalWeb.{Endpoint, PageLive} + + use LokalWeb, :view +end diff --git a/mix.exs b/mix.exs index e12e14b..ef193ed 100644 --- a/mix.exs +++ b/mix.exs @@ -60,6 +60,7 @@ defmodule Lokal.MixProject do {:ex_doc, "~> 0.27", only: :dev, runtime: false}, {:swoosh, "~> 1.6"}, {:gen_smtp, "~> 1.0"}, + {:phoenix_swoosh, "~> 1.0"}, {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 1.0"}, {:gettext, "~> 0.18"}, diff --git a/mix.lock b/mix.lock index 90bcec8..7594d6b 100644 --- a/mix.lock +++ b/mix.lock @@ -40,6 +40,7 @@ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, + "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.0.1", "0db6eb6405a6b06cae4fdf4144659b3f4fee4553e2856fe8a53ba12e9fb21a74", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e34890004baec08f0fa12bd8c77bf64bfb4156b84a07fb79da9322fa94bc3781"}, "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, "plug": {:hex, :plug, "1.13.3", "93b299039c21a8b82cc904d13812bce4ced45cf69153e8d35ca16ffb3e8c5d98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98c8003e4faf7b74a9ac41bee99e328b08f069bf932747d4a7532e97ae837a17"}, "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, diff --git a/readme.md b/readme.md index 4b92dd6..719802d 100644 --- a/readme.md +++ b/readme.md @@ -14,40 +14,42 @@ shopping today! # Installation -1. Clone the repo -2. Run `mix setup` -3. Run `mix phx.server` to start the development server +1. Install [Docker Compose](https://docs.docker.com/compose/install/) or alternatively [Docker Desktop](https://docs.docker.com/desktop/) on your machine. +1. Copy the example [docker-compose.yml](https://gitea.bubbletea.dev/shibao/lokal/src/branch/stable/docker-compose.yml). into your local machine where you want. + Bind mounts are created in the same directory by default. +1. Set the configuration variables in `docker-compose.yml`. You'll need to run + `docker run -it shibaobun/lokal /app/priv/random.sh` to generate a new + secret key base. +1. Use `docker-compose up` or `docker-compose up -d` to start the container! + +The first created user will be created as an admin. # Configuration -For development, I recommend setting environment variables with -[direnv](https://direnv.net). +You can use the following environment variables to configure Lokal in +[docker-compose.yml](https://gitea.bubbletea.dev/shibao/lokal/src/branch/stable/docker-compose.yml). -## `MIX_ENV=dev` - -In `dev` mode, Lokal will listen for these environment variables on compile. - -- `HOST`: External url to generate links with. Set these especially if you're - behind a reverse proxy. Defaults to `localhost`. -- `PORT`: External port for urls. Defaults to `443`. -- `DATABASE_URL`: Controls the database url to connect to. Defaults to - `ecto://postgres:postgres@localhost/lokal_dev`. - -## `MIX_ENV=prod` - -In `prod` mode (or in the Docker container), Lokal will listen for these environment variables at runtime. - -- `HOST`: External url to generate links with. Set these especially if you're - behind a reverse proxy. Defaults to `localhost`. -- `PORT`: Internal port to bind to. Defaults to `4000` and attempts to bind to - `0.0.0.0`. Must be reverse proxied! +- `HOST`: External url to generate links with. Must be set with your hosted + domain name! I.e. `lokal.mywebsite.tld` +- `PORT`: Internal port to bind to. Defaults to `4000`. Must be reverse proxied! - `DATABASE_URL`: Controls the database url to connect to. Defaults to `ecto://postgres:postgres@lokal-db/lokal`. -- `ECTO_IPV6`: Controls if Ecto should use ipv6 to connect to PostgreSQL. +- `ECTO_IPV6`: If set to `true`, Ecto should use ipv6 to connect to PostgreSQL. Defaults to `false`. - `POOL_SIZE`: Controls the pool size to use with PostgreSQL. Defaults to `10`. - `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated - with `mix phx.gen.secret` and set for server to start. + with `docker run -it shibaobun/lokal mix phx.gen.secret` and set for server to start. +- `REGISTRATION`: Controls if user sign-up should be invite only or set to + public. Set to `public` to enable public registration. Defaults to `invite`. +- `LOCALE`: Sets a custom locale. Defaults to `en_US`. +- `SMTP_HOST`: The url for your SMTP email provider. Must be set +- `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`. +- `SMTP_USERNAME`: The username for your SMTP relay. Must be set! +- `SMTP_PASSWORD`: The password for your SMTP relay. Must be set! +- `SMTP_SSL`: Set to `true` to enable SSL for emails. Defaults to `false`. +- `EMAIL_FROM`: Sets the sender email in sent emails. Defaults to + `no-reply@HOST` where `HOST` was previously defined. +- `EMAIL_NAME`: Sets the sender name in sent emails. Defaults to "Lokal". ---