add emails

This commit is contained in:
shibao 2022-02-25 21:35:12 -05:00
parent 34118299e9
commit 6096a420d0
22 changed files with 462 additions and 87 deletions

View File

@ -18,7 +18,8 @@ config :lokal, LokalWeb.Endpoint,
secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I", secret_key_base: "KH59P0iZixX5gP/u+zkxxG8vAAj6vgt0YqnwEB5JP5K+E567SsqkCz69uWShjE7I",
render_errors: [view: LokalWeb.ErrorView, accepts: ~w(html json), layout: false], render_errors: [view: LokalWeb.ErrorView, accepts: ~w(html json), layout: false],
pubsub_server: Lokal.PubSub, 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 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. # Swoosh API client is needed for adapters other than SMTP.
config :swoosh, :api_client, false 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) # Configure esbuild (the version is required)
# config :esbuild, # config :esbuild,
# version: "0.14.0", # version: "0.14.0",

View File

@ -2,9 +2,6 @@ import Config
# Configure your database # Configure your database
config :lokal, Lokal.Repo, config :lokal, Lokal.Repo,
url:
System.get_env("DATABASE_URL") ||
"ecto://postgres:postgres@localhost/lokal_dev",
show_sensitive_data_on_connection_error: true, show_sensitive_data_on_connection_error: true,
pool_size: 10 pool_size: 10
@ -15,13 +12,10 @@ config :lokal, Lokal.Repo,
# watchers to your application. For example, we use it # watchers to your application. For example, we use it
# with esbuild to bundle .js and .css sources. # with esbuild to bundle .js and .css sources.
config :lokal, LokalWeb.Endpoint, 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, check_origin: false,
code_reloader: true, code_reloader: true,
debug_errors: true, debug_errors: true,
secret_key_base: "cSLRa17z1D1qLwQuaw73DMT7BX8oDMkru/rJIsmCdlFypLGRQW3bpqJRrZQtoZJQ", secret_key_base: "dg2lccMgaY3+ZeKppR+ondk4ZRaANZGIN0LMZT1u1uzscH4jO5W9a9b9V9BkC+MW",
watchers: [ watchers: [
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args) # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
# esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} # esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}

View File

@ -9,16 +9,7 @@ import Config
# manifest is generated by the `mix phx.digest` task, # manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and # which you should run after static files are built and
# before starting your production server. # before starting your production server.
config :lokal, LokalWeb.Endpoint, config :lokal, LokalWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
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
# Do not print debug messages in production # Do not print debug messages in production
config :logger, level: :info config :logger, level: :info

View File

@ -12,12 +12,30 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
config :lokal, LokalWeb.Endpoint, server: true config :lokal, LokalWeb.Endpoint, server: true
end end
if config_env() == :prod do 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 = 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") || System.get_env("DATABASE_URL") ||
"ecto://postgres:postgres@lokal-db/lokal" "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`."
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, config :lokal, Lokal.Repo,
# ssl: true, # ssl: true,
@ -25,6 +43,18 @@ if config_env() == :prod do
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6 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. # 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 # 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 # 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 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, # Automatically apply migrations
ururl: [scheme: "https", host: host, port: 443], config :lokal, Lokal.Application, automigrate: true
http: [
# Enable IPv6 and bind on all interfaces. # Set up SMTP settings
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. config :lokal, Lokal.Mailer,
# See the documentation on https://hexdocs.pm/plug_cowboy/Plug.Cowboy.html adapter: Swoosh.Adapters.SMTP,
# for details about using IPv6 vs IPv4 and loopback vs public addresses. relay: System.get_env("SMTP_HOST") || raise("No SMTP_HOST set!"),
ip: {0, 0, 0, 0, 0, 0, 0, 0}, port: System.get_env("SMTP_PORT") || 587,
port: String.to_integer(System.get_env("PORT") || "4000") username: System.get_env("SMTP_USERNAME") || raise("No SMTP_USERNAME set!"),
], password: System.get_env("SMTP_PASSWORD") || raise("No SMTP_PASSWORD set!"),
secret_key_base: secret_key_base, ssl: System.get_env("SMTP_SSL") == "true",
server: true email_from: System.get_env("EMAIL_FROM") || "no-reply@#{System.get_env("HOST")}",
email_name: System.get_env("EMAIL_NAME") || "Lokal"
# ## Using releases # ## Using releases
# #
@ -61,22 +92,4 @@ if config_env() == :prod do
# #
# Then you can assemble a release by calling `mix release`. # Then you can assemble a release by calling `mix release`.
# See `mix help release` for more information. # 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 end

View File

@ -1,14 +1,14 @@
import Config import Config
# Only in tests, remove the complexity from the password hashing algorithm
config :bcrypt_elixir, :log_rounds, 1
# Configure your database # Configure your database
# #
# The MIX_TEST_PARTITION environment variable can be used # The MIX_TEST_PARTITION environment variable can be used
# to provide built-in test partitioning in CI environment. # to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information. # Run `mix help test` for more information.
config :lokal, Lokal.Repo, 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: Ecto.Adapters.SQL.Sandbox,
pool_size: 10 pool_size: 10
@ -16,7 +16,7 @@ config :lokal, Lokal.Repo,
# you can enable the server option below. # you can enable the server option below.
config :lokal, LokalWeb.Endpoint, config :lokal, LokalWeb.Endpoint,
http: [ip: {0, 0, 0, 0}, port: 4002], http: [ip: {0, 0, 0, 0}, port: 4002],
secret_key_base: "T4DkRImgeMNCcPcTWBCZyKYp3KQ8yyPD33VT4wj6ogbP8fIGUsqmOTNX3clTMrLo", secret_key_base: "S3qq9QtUdsFtlYej+HTjAVN95uP5i5tf2sPYINWSQfCKJghFj2B1+wTAoljZyHOK",
server: false server: false
# In test we don't send emails. # In test we don't send emails.
@ -27,3 +27,6 @@ config :logger, level: :warn
# Initialize plugs at runtime for faster test compilation # Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime config :phoenix, :plug_init_mode, :runtime
# Disable Oban
config :lokal, Oban, queues: false, plugins: false

124
contributing.md Normal file
View File

@ -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".

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,42 @@
defmodule Lokal.Mailer do defmodule Lokal.Mailer do
@moduledoc """ @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 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 end

View File

@ -0,0 +1,25 @@
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
<span style="margin-bottom: 0.75em; font-size: 1.5em;">
<%= dgettext("emails", "Hi %{email},", email: @user.email) %>
</span>
<br/>
<span style="margin-bottom: 1em; font-size: 1.25em;">
<%= dgettext("emails", "Welcome to %{name}!", name: "Lokal") %>
</span>
<br/>
<%= dgettext("emails", "You can confirm your account by visiting the URL below:") %>
<br/>
<a style="margin: 1em; color: rgb(31, 31, 31);" href="<%= @url %>"><%= @url %></a>
<br/>
<%= dgettext("emails",
"If you didn't create an account at %{name}, please ignore this.",
name: "Lokal") %>
</div>

View File

@ -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)) %>

View File

@ -0,0 +1,19 @@
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
<span style="margin-bottom: 0.5em; font-size: 1.5em;">
<%= dgettext("emails", "Hi %{email},", email: @user.email) %>
</span>
<br/>
<%= dgettext("emails", "You can reset your password by visiting the URL below:") %>
<br/>
<a style="margin: 1em; color: rgb(31, 31, 31);" href="<%= @url %>"><%= @url %></a>
<br/>
<%= dgettext("emails",
"If you didn't request this change from %{name}, please ignore this.",
name: "Lokal") %>
</div>

View File

@ -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)) %>

View File

@ -0,0 +1,19 @@
<div style="display: flex; flex-direction: column; justify-content: center; align-items: center;">
<span style="margin-bottom: 0.5em; font-size: 1.5em;">
<%= dgettext("emails", "Hi %{email},", email: @user.email) %>
</span>
<br/>
<%= dgettext("emails", "You can change your email by visiting the URL below:") %>
<br/>
<a style="margin: 1em; color: rgb(31, 31, 31);" href="<%= @url %>"><%= @url %></a>
<br/>
<%= dgettext("emails",
"If you didn't request this change from %{name}, please ignore this.",
name: "Lokal") %>
</div>

View File

@ -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)) %>

View File

@ -0,0 +1,24 @@
<html>
<head>
<title>
<%= @email.subject %>
</title>
</head>
<body
style="padding: 2em; color: rgb(31, 31, 31); background-color: rgb(220, 220, 228); font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; text-align: center;"
>
<%= @inner_content %>
<hr
style="margin: 2em auto; border-width: 1px; border-color: rgb(212, 212, 216); width: 100%; max-width: 42rem;"
/>
<a style="color: rgb(31, 31, 31);" href={Routes.live_url(Endpoint, PageLive)}>
<%= dgettext(
"emails",
"This email was sent from %{name}, the self-hosted firearm tracker website.",
name: "Lokal"
) %>
</a>
</body>
</html>

View File

@ -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)) %>

View File

@ -0,0 +1 @@
<%= @inner_content %>

View File

@ -0,0 +1,8 @@
defmodule LokalWeb.EmailView do
@moduledoc """
A view for email-related helper functions
"""
alias LokalWeb.{Endpoint, PageLive}
use LokalWeb, :view
end

View File

@ -60,6 +60,7 @@ defmodule Lokal.MixProject do
{:ex_doc, "~> 0.27", only: :dev, runtime: false}, {:ex_doc, "~> 0.27", only: :dev, runtime: false},
{:swoosh, "~> 1.6"}, {:swoosh, "~> 1.6"},
{:gen_smtp, "~> 1.0"}, {:gen_smtp, "~> 1.0"},
{:phoenix_swoosh, "~> 1.0"},
{:telemetry_metrics, "~> 0.6"}, {:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"}, {:telemetry_poller, "~> 1.0"},
{:gettext, "~> 0.18"}, {:gettext, "~> 0.18"},

View File

@ -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_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_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_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"}, "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": {: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"}, "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"},

View File

@ -14,40 +14,42 @@ shopping today!
# Installation # Installation
1. Clone the repo 1. Install [Docker Compose](https://docs.docker.com/compose/install/) or alternatively [Docker Desktop](https://docs.docker.com/desktop/) on your machine.
2. Run `mix setup` 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.
3. Run `mix phx.server` to start the development server 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 # Configuration
For development, I recommend setting environment variables with You can use the following environment variables to configure Lokal in
[direnv](https://direnv.net). [docker-compose.yml](https://gitea.bubbletea.dev/shibao/lokal/src/branch/stable/docker-compose.yml).
## `MIX_ENV=dev` - `HOST`: External url to generate links with. Must be set with your hosted
domain name! I.e. `lokal.mywebsite.tld`
In `dev` mode, Lokal will listen for these environment variables on compile. - `PORT`: Internal port to bind to. Defaults to `4000`. Must be reverse proxied!
- `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!
- `DATABASE_URL`: Controls the database url to connect to. Defaults to - `DATABASE_URL`: Controls the database url to connect to. Defaults to
`ecto://postgres:postgres@lokal-db/lokal`. `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`. Defaults to `false`.
- `POOL_SIZE`: Controls the pool size to use with PostgreSQL. Defaults to `10`. - `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 - `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".
--- ---