fix(portal): support dark mode in outbound emails (#10493)

Ensure that users with dark mode enabled system-wide get nice experience
whilst reading the emails.

Add a `mix test_emails` task to send all the emails and quickly inspect
them locally.

Before:

<img width="767" height="924" alt="image"
src="https://github.com/user-attachments/assets/aaac75bd-67ad-4fd8-82e8-6726ffea6bae"
/>


After (viewed via `mix test_emails`):

<img width="1063" height="928" alt="image"
src="https://github.com/user-attachments/assets/57d3a4d9-5b8f-4a45-8546-7615e15422d8"
/>

---------

Signed-off-by: Mariusz Klochowicz <mariusz@klochowicz.com>
Co-authored-by: Brian Manifold <bmanifold@users.noreply.github.com>
This commit is contained in:
Mariusz Klochowicz
2025-10-17 06:05:36 +10:30
committed by GitHub
parent bf91021e2e
commit a27676a903
8 changed files with 535 additions and 41 deletions

View File

@@ -41,12 +41,13 @@
line-height: 32px !important
}
}
<%= File.read!(Application.app_dir(:domain, "priv/static/emails/dark_mode.css")) %>
</style>
</head>
<body style="margin: 0; width: 100%; background-color: #f6f6f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word">
<div role="article" aria-roledescription="email" aria-label="Welcome to Firezone" lang="en">
<div
class="sm-px-4"
class="sm-px-4 email-container"
style="background-color: #f6f6f6; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif"
>
<table align="center" cellpadding="0" cellspacing="0" role="none">
@@ -59,18 +60,26 @@
<a href="https://firezone.dev?utm_source=email">
<div>
<img
class="logo-light"
src="https://www.firezone.dev/images/logo-lockup.png"
width="250"
alt="Firezone logo"
style="max-width: 100%; vertical-align: middle; line-height: 1"
/>
<img
class="logo-dark"
src="https://www.firezone.dev/images/logo-text-dark.svg"
width="250"
alt="Firezone logo"
style="display: none; max-width: 100%; vertical-align: middle; line-height: 1"
/>
</div>
</a>
</div>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="none">
<tr>
<td
class="sm-px-6"
class="sm-px-6 content-box"
style="border-radius: 4px; background-color: #ffffff; padding: 48px; font-size: 16px; color: #4f4f4f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)"
>
<h1
@@ -121,6 +130,7 @@
<p></p>
<div
role="separator"
class="separator"
style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0"
>
&zwj;
@@ -136,7 +146,7 @@
<td style="line-height: 48px">&zwj;</td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<td class="footer-text" style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<p style="margin: 0; font-style: italic">
Blazing-fast alternative to legacy VPNs
</p>
@@ -144,7 +154,7 @@
<a
href="https://www.firezone.dev/kb?utm_source=email"
class="hover-important-text-decoration-underline"
style="color: #37007f; text-decoration: none"
style="color: #5e00d6; text-decoration: none"
>
Docs
</a>
@@ -152,15 +162,15 @@
<a
href="https://github.com/firezone?utm_source=email"
class="hover-important-text-decoration-underline"
style="color: #37007f; text-decoration: none;"
style="color: #5e00d6; text-decoration: none;"
>
Github
GitHub
</a>
&bull;
<a
href="https://x.com/firezonehq?utm_source=email"
class="hover-important-text-decoration-underline"
style="color: #37007f; text-decoration: none;"
style="color: #5e00d6; text-decoration: none;"
>
X
</a>

View File

@@ -49,12 +49,13 @@
line-height: 32px !important
}
}
<%= File.read!(Application.app_dir(:domain, "priv/static/emails/dark_mode.css")) %>
</style>
</head>
<body style="margin: 0; width: 100%; background-color: #f6f6f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word">
<div role="article" aria-roledescription="email" aria-label="Firezone Sign In Token" lang="en">
<div
class="sm-px-4"
class="sm-px-4 email-container"
style="background-color: #f6f6f6; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif"
>
<table align="center" cellpadding="0" cellspacing="0" role="none">
@@ -67,18 +68,26 @@
<a href="https://firezone.dev?utm_source=email">
<div>
<img
class="logo-light"
src="https://www.firezone.dev/images/logo-lockup.png"
width="250"
alt="Firezone logo"
style="max-width: 100%; vertical-align: middle; line-height: 1"
/>
<img
class="logo-dark"
src="https://www.firezone.dev/images/logo-text-dark.svg"
width="250"
alt="Firezone logo"
style="display: none; max-width: 100%; vertical-align: middle; line-height: 1"
/>
</div>
</a>
</div>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="none">
<tr>
<td
class="sm-px-6"
class="sm-px-6 content-box"
style="border-radius: 4px; background-color: #ffffff; padding: 48px; font-size: 16px; color: #4f4f4f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)"
>
<h1
@@ -120,6 +129,7 @@
</p>
<div
role="separator"
class="separator"
style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 16px 0"
>
&zwj;
@@ -183,6 +193,7 @@
<p></p>
<div
role="separator"
class="separator"
style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;"
>
&zwj;
@@ -200,7 +211,7 @@
<td style="line-height: 48px">&zwj;</td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<td class="footer-text" style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<p style="margin: 0; font-style: italic;">
Blazing-fast alternative to legacy VPNs
</p>
@@ -208,7 +219,7 @@
<a
href="https://www.firezone.dev/kb?utm_source=email"
class="hover-important-text-decoration-underline"
style="color: #37007f; text-decoration: none"
style="color: #5e00d6; text-decoration: none"
>
Docs
</a>
@@ -216,15 +227,15 @@
<a
href="https://github.com/firezone?utm_source=email"
class="hover-important-text-decoration-underline"
style="color: #37007f; text-decoration: none;"
style="color: #5e00d6; text-decoration: none;"
>
Github
GitHub
</a>
&bull;
<a
href="https://x.com/firezonehq?utm_source=email"
class="hover-important-text-decoration-underline"
style="color: #37007f; text-decoration: none;"
style="color: #5e00d6; text-decoration: none;"
>
X
</a>

View File

@@ -49,12 +49,13 @@
line-height: 32px !important
}
}
<%= File.read!(Application.app_dir(:domain, "priv/static/emails/dark_mode.css")) %>
</style>
</head>
<body style="margin: 0; width: 100%; background-color: #f6f6f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word">
<div role="article" aria-roledescription="email" aria-label="Welcome to Firezone" lang="en">
<div
class="sm-px-4"
class="sm-px-4 email-container"
style="background-color: #f6f6f6; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif"
>
<table align="center" cellpadding="0" cellspacing="0" role="none">
@@ -67,18 +68,26 @@
<a href="https://firezone.dev?utm_source=email">
<div>
<img
class="logo-light"
src="https://www.firezone.dev/images/logo-lockup.png"
width="250"
alt="Firezone logo"
style="max-width: 100%; vertical-align: middle; line-height: 1"
/>
<img
class="logo-dark"
src="https://www.firezone.dev/images/logo-text-dark.svg"
width="250"
alt="Firezone logo"
style="display: none; max-width: 100%; vertical-align: middle; line-height: 1"
/>
</div>
</a>
</div>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="none">
<tr>
<td
class="sm-px-6"
class="sm-px-6 content-box"
style="border-radius: 4px; background-color: #ffffff; padding: 48px; font-size: 16px; color: #4f4f4f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)"
>
<h1
@@ -111,6 +120,7 @@
</div>
<div
role="separator"
class="separator"
style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0"
>
&zwj;
@@ -162,6 +172,7 @@
<p></p>
<div
role="separator"
class="separator"
style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;"
>
&zwj;
@@ -176,7 +187,7 @@
<td style="line-height: 48px">&zwj;</td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<td class="footer-text" style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<p style="margin: 0; font-style: italic">
Blazing-fast alternative to legacy VPNs
</p>
@@ -184,7 +195,7 @@
<a
href="https://www.firezone.dev/kb?utm_source=email"
class="hover-important-text-decoration-underline"
style="color: #37007f; text-decoration: none"
style="color: #5e00d6; text-decoration: none"
>
Docs
</a>
@@ -192,15 +203,15 @@
<a
href="https://github.com/firezone?utm_source=email"
class="hover-important-text-decoration-underline"
style="color: #37007f; text-decoration: none;"
style="color: #5e00d6; text-decoration: none;"
>
Github
GitHub
</a>
&bull;
<a
href="https://x.com/firezonehq?utm_source=email"
class="hover-important-text-decoration-underline"
style="color: #37007f; text-decoration: none;"
style="color: #5e00d6; text-decoration: none;"
>
X
</a>

View File

@@ -44,6 +44,7 @@
.rtl-text-right:where([dir="rtl"], [dir="rtl"] *) {
text-align: right !important
}
<%= File.read!(Application.app_dir(:domain, "priv/static/emails/dark_mode.css")) %>
</style>
</head>
<body style="margin: 0; width: 100%; background-color: #f6f6f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word">
@@ -51,20 +52,21 @@
Firezone Gateway Upgrade Available
</div>
<div role="article" aria-roledescription="email" aria-label="Firezone Gateway Upgrade Available" lang="en">
<div class="sm-px-4" style="background-color: #f6f6f6; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif">
<div class="sm-px-4 email-container" style="background-color: #f6f6f6; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif">
<table align="center" cellpadding="0" cellspacing="0" role="none">
<tr>
<td style="width: 552px; max-width: 100%">
<div class="sm-my-8" style="margin-top: 48px; margin-bottom: 48px; text-align: center">
<a href="https://firezone.dev">
<div>
<img src="https://www.firezone.dev/images/logo-lockup.png" width="250" alt="Firezone logo" style="max-width: 100%; vertical-align: middle; line-height: 1">
<img class="logo-light" src="https://www.firezone.dev/images/logo-lockup.png" width="250" alt="Firezone logo" style="max-width: 100%; vertical-align: middle; line-height: 1">
<img class="logo-dark" src="https://www.firezone.dev/images/logo-text-dark.svg" width="250" alt="Firezone logo" style="display: none; max-width: 100%; vertical-align: middle; line-height: 1">
</div>
</a>
</div>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="none">
<tr>
<td class="sm-px-6" style="border-radius: 4px; background-color: #ffffff; padding: 48px; font-size: 16px; color: #4f4f4f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)">
<td class="sm-px-6 content-box" style="border-radius: 4px; background-color: #ffffff; padding: 48px; font-size: 16px; color: #4f4f4f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)">
<h1 class="sm-leading-8" style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #000000">
Gateway Upgrade Available!
</h1>
@@ -78,7 +80,7 @@
<p style="margin: 0; line-height: 24px;">
The following list of Gateways in your Firezone Account can be upgraded.
</p>
<div role="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0">&zwj;</div>
<div role="separator" class="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0">&zwj;</div>
<p style="margin: 0 0 16 0; line-height: 24px;">
<span style="margin-bottom: 32px; font-weight: 500; text-decoration-line: underline; text-underline-offset: 2px">Outdated Gateways</span>
</p>
@@ -95,7 +97,7 @@
<% end %>
</table>
<p></p>
<div role="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;">&zwj;</div>
<div role="separator" class="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;">&zwj;</div>
<%= if @incompatible_client_count > 0 do %>
<p style="margin: 0; line-height: 24px;">
<span style="font-weight: 600; color: #6b6b6b">WARNING:</span>
@@ -124,16 +126,16 @@
<td style="line-height: 48px">&zwj;</td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<td class="footer-text" style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<p style="margin: 0; font-style: italic">
Blazing-fast alternative to legacy VPNs
</p>
<p style="cursor: default">
<a href="https://www.firezone.dev/kb" class="hover-important-text-decoration-underline" style="color: #37007f; text-decoration: none">Docs</a>
<a href="https://www.firezone.dev/kb" class="hover-important-text-decoration-underline" style="color: #5e00d6; text-decoration: none">Docs</a>
&bull;
<a href="https://github.com/firezone" class="hover-important-text-decoration-underline" style="color: #37007f; text-decoration: none;">Github</a>
<a href="https://github.com/firezone" class="hover-important-text-decoration-underline" style="color: #5e00d6; text-decoration: none;">GitHub</a>
&bull;
<a href="https://x.com/firezonehq" class="hover-important-text-decoration-underline" style="color: #37007f; text-decoration: none;">X</a>
<a href="https://x.com/firezonehq" class="hover-important-text-decoration-underline" style="color: #5e00d6; text-decoration: none;">X</a>
</p>
</td>
</tr>

View File

@@ -44,24 +44,26 @@
line-height: 32px !important
}
}
<%= File.read!(Application.app_dir(:domain, "priv/static/emails/dark_mode.css")) %>
</style>
</head>
<body style="margin: 0; width: 100%; background-color: #f6f6f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word">
<div role="article" aria-roledescription="email" aria-label="Firezone Sync Error" lang="en">
<div class="sm-px-4" style="background-color: #f6f6f6; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif">
<div class="sm-px-4 email-container" style="background-color: #f6f6f6; font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif">
<table align="center" cellpadding="0" cellspacing="0" role="none">
<tr>
<td style="width: 552px; max-width: 100%">
<div class="sm-my-8" style="margin-top: 48px; margin-bottom: 48px; text-align: center">
<a href="https://firezone.dev">
<div>
<img src="https://www.firezone.dev/images/logo-lockup.png" width="250" alt="Firezone logo" style="max-width: 100%; vertical-align: middle; line-height: 1">
<img class="logo-light" src="https://www.firezone.dev/images/logo-lockup.png" width="250" alt="Firezone logo" style="max-width: 100%; vertical-align: middle; line-height: 1">
<img class="logo-dark" src="https://www.firezone.dev/images/logo-text-dark.svg" width="250" alt="Firezone logo" style="display: none; max-width: 100%; vertical-align: middle; line-height: 1">
</div>
</a>
</div>
<table style="width: 100%;" cellpadding="0" cellspacing="0" role="none">
<tr>
<td class="sm-px-6" style="border-radius: 4px; background-color: #ffffff; padding: 48px; font-size: 16px; color: #4f4f4f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)">
<td class="sm-px-6 content-box" style="border-radius: 4px; background-color: #ffffff; padding: 48px; font-size: 16px; color: #4f4f4f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)">
<h1 class="sm-leading-8" style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #000000">
<%= @provider.name %> Sync Error!
</h1>
@@ -72,10 +74,10 @@
<p style="margin: 0; line-height: 24px;">
Below is the last sync error message:
<div>
<pre style="margin: 0; white-space: pre; border-radius: 4px; background-color: #000000; padding: 8px 12px; line-height: 24px; color: #e7e7e7"><code><%= @provider.last_sync_error %></code></pre>
<pre style="margin: 0; white-space: pre; border-radius: 4px; background-color: #f3f4f6; padding: 8px 12px; line-height: 24px; color: #1f2937"><code><%= @provider.last_sync_error %></code></pre>
</div>
</p>
<div role="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0">&zwj;</div>
<div role="separator" class="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0">&zwj;</div>
<p style="margin: 0; line-height: 24px;">
<span style="font-weight: 500; text-decoration-line: underline; text-underline-offset: 2px">Identity Provider Details</span>
</p>
@@ -98,7 +100,7 @@
</tr>
</table>
<p></p>
<div role="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;">&zwj;</div>
<div role="separator" class="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;">&zwj;</div>
<p style="margin: 0; line-height: 24px;">
Please verify that all Identity Provider information entered in to Firezone is correct. If the problem persists, please reach out to Firezone support
using Slack or email.
@@ -113,16 +115,16 @@
<td style="line-height: 48px">&zwj;</td>
</tr>
<tr>
<td style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<td class="footer-text" style="padding-left: 24px; padding-right: 24px; text-align: center; font-size: 12px; color: #575757">
<p style="margin: 0; font-style: italic">
Blazing-fast alternative to legacy VPNs
</p>
<p style="cursor: default">
<a href="https://www.firezone.dev/kb" class="hover-important-text-decoration-underline" style="color: #37007f; text-decoration: none">Docs</a>
<a href="https://www.firezone.dev/kb" class="hover-important-text-decoration-underline" style="color: #5e00d6; text-decoration: none">Docs</a>
&bull;
<a href="https://github.com/firezone" class="hover-important-text-decoration-underline" style="color: #37007f; text-decoration: none;">Github</a>
<a href="https://github.com/firezone" class="hover-important-text-decoration-underline" style="color: #5e00d6; text-decoration: none;">GitHub</a>
&bull;
<a href="https://x.com/firezonehq" class="hover-important-text-decoration-underline" style="color: #37007f; text-decoration: none;">X</a>
<a href="https://x.com/firezonehq" class="hover-important-text-decoration-underline" style="color: #5e00d6; text-decoration: none;">X</a>
</p>
</td>
</tr>

View File

@@ -0,0 +1,398 @@
defmodule Mix.Tasks.Email.Render do
@moduledoc """
Render email templates for development and testing purposes.
All emails will appear in the Swoosh mailbox at http://localhost:13000/dev/mailbox
## Usage
# Make sure no iex session is running, then:
$ mix email.render
# Then visit: http://localhost:13000/dev/mailbox
# The task will:
# 1. Start the application
# 2. Generate test emails
# 3. Keep running so you can view the emails
# 4. Press Ctrl+C twice to exit when done
## Examples
# Send all test emails
$ mix email.render
# Send specific emails
$ mix email.render sign_up
$ mix email.render sign_in
$ mix email.render new_user
$ mix email.render outdated_gateway
$ mix email.render sync_error
## Testing Dark Mode
1. Open an email in the mailbox at http://localhost:13000/dev/mailbox
2. Toggle macOS system appearance: System Settings → Appearance → Dark
3. Or use browser dev tools to emulate prefers-color-scheme: dark
## Note
The application stays running after sending emails so you can view them in the
mailbox. Emails are stored in memory and will be lost when the app exits.
"""
@shortdoc "Render email templates for development"
use Mix.Task
alias Domain.{Accounts, Auth, Actors, Repo, Mailer}
@impl true
def run(args) do
# Start the application (including Repo, Swoosh, and all services)
Mix.Task.run("app.start")
case args do
[] ->
send_all_test_emails()
keep_running()
["sign_up"] ->
send_sign_up_link_email()
keep_running()
["sign_in"] ->
send_sign_in_link_email()
keep_running()
["new_user"] ->
send_new_user_email()
keep_running()
["outdated_gateway"] ->
send_outdated_gateway_email()
keep_running()
["sync_error"] ->
send_sync_error_email()
keep_running()
_ ->
Mix.shell().error("""
Unknown argument: #{Enum.join(args, " ")}
Valid options:
mix email.render # Send all test emails
mix email.render sign_up
mix email.render sign_in
mix email.render new_user
mix email.render outdated_gateway
mix email.render sync_error
""")
exit({:shutdown, 1})
end
end
defp send_all_test_emails do
Mix.shell().info("\n🚀 Generating test emails...")
with {:ok, _} <- send_sign_up_link_email(),
{:ok, _} <- send_sign_in_link_email(),
{:ok, _} <- send_new_user_email(),
{:ok, _} <- send_outdated_gateway_email(),
{:ok, _} <- send_sync_error_email() do
Mix.shell().info("\n✅ All test emails sent successfully!")
:ok
else
{:error, reason} ->
Mix.shell().error("\n❌ Error sending emails: #{inspect(reason)}\n")
exit({:shutdown, 1})
end
end
defp keep_running do
Mix.shell().info("\n📬 Open http://localhost:13000/dev/mailbox to view the emails")
Mix.shell().info("\n⏳ Keeping app running so you can view the emails...")
Mix.shell().info(" Press Ctrl+C twice to exit when done.\n")
# Keep the app running so emails stay in memory
:timer.sleep(:infinity)
end
defp send_sign_up_link_email do
Mix.shell().info("📧 Generating sign-up welcome email...")
account = get_or_create_test_account()
provider = get_or_create_email_provider(account)
actor = get_or_create_test_actor(account, :account_admin_user)
identity = get_or_create_identity(account, provider, actor)
email =
Mailer.AuthEmail.sign_up_link_email(
account,
identity,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
{127, 0, 0, 1}
)
Mailer.deliver(email)
end
defp send_sign_in_link_email do
Mix.shell().info("📧 Generating sign-in token email...")
account = get_or_create_test_account()
provider = get_or_create_email_provider(account)
actor = get_or_create_test_actor(account, :account_user)
identity = get_or_create_identity(account, provider, actor)
# Set up the identity with token state
identity =
identity
|> Ecto.Changeset.change(
provider_state: %{
"token_created_at" => DateTime.utc_now()
}
)
|> Repo.update!()
|> Repo.preload(:account)
secret = "ABC123XYZ789"
email =
Mailer.AuthEmail.sign_in_link_email(
identity,
secret,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
{127, 0, 0, 1}
)
Mailer.deliver(email)
end
defp send_new_user_email do
Mix.shell().info("📧 Generating new user invitation email...")
account = get_or_create_test_account()
provider = get_or_create_email_provider(account)
admin_actor = get_or_create_test_actor(account, :account_admin_user, "Admin User")
admin_identity = get_or_create_identity(account, provider, admin_actor, "admin@test.local")
new_actor = get_or_create_test_actor(account, :account_user, "New User", "new_user")
new_identity = get_or_create_identity(account, provider, new_actor, "newuser@test.local")
subject = %Auth.Subject{
account: account,
actor: admin_actor,
identity: admin_identity,
permissions: MapSet.new(),
token_id: Ecto.UUID.generate(),
expires_at: DateTime.add(DateTime.utc_now(), 3600, :second),
context: %Auth.Context{
type: :browser,
remote_ip: {127, 0, 0, 1},
user_agent: "Mozilla/5.0 (Test)"
}
}
email = Mailer.AuthEmail.new_user_email(account, new_identity, subject)
Mailer.deliver(email)
end
defp send_outdated_gateway_email do
Mix.shell().info("📧 Generating outdated gateway notification email...")
# Create a test gateway
account = get_or_create_test_account()
# Create a gateway group
group = get_or_create_gateway_group(account)
gateway1 =
Repo.insert!(
%Domain.Gateways.Gateway{
account_id: account.id,
group_id: group.id,
external_id: "test-gateway-us-east",
name: "Gateway US East",
public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(),
last_seen_user_agent: "Linux/1.0.0",
last_seen_remote_ip: %Postgrex.INET{address: {127, 0, 0, 1}},
last_seen_version: "1.0.0",
last_seen_at: DateTime.utc_now()
},
on_conflict: :nothing
)
gateway2 =
Repo.insert!(
%Domain.Gateways.Gateway{
account_id: account.id,
group_id: group.id,
external_id: "test-gateway-eu-west",
name: "Gateway EU West",
public_key: :crypto.strong_rand_bytes(32) |> Base.encode64(),
last_seen_user_agent: "Linux/1.0.1",
last_seen_remote_ip: %Postgrex.INET{address: {127, 0, 0, 1}},
last_seen_version: "1.0.1",
last_seen_at: DateTime.utc_now()
},
on_conflict: :nothing
)
admin_actor = get_or_create_test_actor(account, :account_admin_user)
provider = get_or_create_email_provider(account)
admin_identity = get_or_create_identity(account, provider, admin_actor)
# Set incompatible_client_count to 3 to trigger the optional warning section
email =
Mailer.Notifications.outdated_gateway_email(
account,
[gateway1, gateway2],
3,
admin_identity.provider_identifier
)
Mailer.deliver(email)
end
defp send_sync_error_email do
Mix.shell().info("📧 Generating sync error email...")
account = get_or_create_test_account()
# Create or get a test OIDC provider with sync error
provider =
case Repo.get_by(Auth.Provider, account_id: account.id, name: "Okta Directory Sync Test") do
nil ->
Repo.insert!(%Auth.Provider{
account_id: account.id,
name: "Okta Directory Sync Test",
adapter: :openid_connect,
adapter_state: %{},
adapter_config: %{
"discovery_document_uri" =>
"https://dev-123456.okta.com/.well-known/openid-configuration",
"client_id" => "test_client_id",
"client_secret" => "test_client_secret",
"response_type" => "code",
"scope" => "openid email profile"
},
created_by: :system,
created_by_subject: %{"name" => "System", "email" => nil},
provisioner: :manual,
last_sync_error:
"Connection timeout: Unable to reach identity provider API at https://dev-123456.okta.com",
last_syncs_failed: 3
})
provider ->
provider
end
admin_actor = get_or_create_test_actor(account, :account_admin_user)
email_provider = get_or_create_email_provider(account)
admin_identity = get_or_create_identity(account, email_provider, admin_actor)
# Preload the account association
provider = Repo.preload(provider, :account)
email =
Mailer.SyncEmail.sync_error_email(
provider,
admin_identity.provider_identifier
)
Mailer.deliver(email)
end
# Helper functions
defp get_or_create_test_account do
case Repo.get_by(Accounts.Account, slug: "test_email_account") do
nil ->
{:ok, account} =
Accounts.create_account(%{
name: "Test Email Account",
slug: "test_email_account"
})
account
account ->
account
end
end
defp get_or_create_email_provider(account) do
case Repo.get_by(Auth.Provider, account_id: account.id, adapter: :email) do
nil ->
{:ok, provider} =
Auth.create_provider(account, %{
name: "Email",
adapter: :email,
adapter_config: %{},
created_by: :system,
provisioner: :manual
})
provider
provider ->
provider
end
end
defp get_or_create_test_actor(account, type, name \\ "Test User", slug_suffix \\ "test") do
slug = "test-actor-#{slug_suffix}"
case Repo.get_by(Actors.Actor, account_id: account.id, name: slug) do
nil ->
Repo.insert!(%Actors.Actor{
account_id: account.id,
type: type,
name: name
})
actor ->
actor
end
end
defp get_or_create_identity(account, provider, actor, email \\ "test@test.local") do
case Repo.get_by(Auth.Identity,
account_id: account.id,
provider_id: provider.id,
actor_id: actor.id
) do
nil ->
{:ok, identity} =
Auth.upsert_identity(actor, provider, %{
provider_identifier: email,
provider_identifier_confirmation: email,
provider_virtual_state: %{}
})
identity
identity ->
identity
end
end
defp get_or_create_gateway_group(account) do
case Repo.get_by(Domain.Gateways.Group, account_id: account.id, name: "Test Gateway Group") do
nil ->
Repo.insert!(%Domain.Gateways.Group{
account_id: account.id,
name: "Test Gateway Group",
managed_by: :account,
created_by: :system
})
group ->
group
end
end
end

View File

@@ -0,0 +1,59 @@
@media (prefers-color-scheme: dark) {
body {
background-color: #1a1a1a !important;
}
.email-container {
background-color: #1a1a1a !important;
}
.content-box {
background-color: #2d2d2d !important;
color: #e5e5e5 !important;
}
.content-box h1,
.content-box h2 {
color: #ffffff !important;
}
.content-box p,
.content-box td {
color: #e5e5e5 !important;
}
.content-box th {
color: #d4d4d4 !important;
}
.content-box table tr {
background-color: #2d2d2d !important;
}
.content-box code {
background-color: #1a1a1a !important;
border-color: #525252 !important;
color: #e5e5e5 !important;
}
.content-box pre {
background-color: #1a1a1a !important;
color: #e5e5e5 !important;
}
.separator {
background-color: #525252 !important;
}
.footer-text {
color: #a3a3a3 !important;
}
.footer-text a {
color: #a78bfa !important;
}
.logo-light {
display: none !important;
max-height: 0 !important;
overflow: hidden !important;
}
.logo-dark {
display: inline-block !important;
}
}
@media (prefers-color-scheme: light) {
.logo-dark {
display: none !important;
max-height: 0 !important;
overflow: hidden !important;
}
}

View File

@@ -21,7 +21,8 @@ defmodule Firezone.MixProject do
],
deps: deps(),
dialyzer: [
plt_file: {:no_warn, "priv/plts/dialyzer.plt"}
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
plt_add_apps: [:mix]
],
aliases: aliases(),
releases: releases()