mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 02:18:47 +00:00
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:
committed by
GitHub
parent
bf91021e2e
commit
a27676a903
@@ -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"
|
||||
>
|
||||
‍
|
||||
@@ -136,7 +146,7 @@
|
||||
<td style="line-height: 48px">‍</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>
|
||||
•
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
‍
|
||||
@@ -183,6 +193,7 @@
|
||||
<p></p>
|
||||
<div
|
||||
role="separator"
|
||||
class="separator"
|
||||
style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;"
|
||||
>
|
||||
‍
|
||||
@@ -200,7 +211,7 @@
|
||||
<td style="line-height: 48px">‍</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>
|
||||
•
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
‍
|
||||
@@ -162,6 +172,7 @@
|
||||
<p></p>
|
||||
<div
|
||||
role="separator"
|
||||
class="separator"
|
||||
style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;"
|
||||
>
|
||||
‍
|
||||
@@ -176,7 +187,7 @@
|
||||
<td style="line-height: 48px">‍</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>
|
||||
•
|
||||
<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>
|
||||
|
||||
@@ -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">‍</div>
|
||||
<div role="separator" class="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0">‍</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;">‍</div>
|
||||
<div role="separator" class="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;">‍</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">‍</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>
|
||||
•
|
||||
<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>
|
||||
•
|
||||
<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>
|
||||
|
||||
@@ -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">‍</div>
|
||||
<div role="separator" class="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0">‍</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;">‍</div>
|
||||
<div role="separator" class="separator" style="background-color: #d1d1d1; height: 1px; line-height: 1px; margin: 32px 0;">‍</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">‍</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>
|
||||
•
|
||||
<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>
|
||||
•
|
||||
<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>
|
||||
|
||||
398
elixir/apps/domain/lib/mix/tasks/email/render.ex
Normal file
398
elixir/apps/domain/lib/mix/tasks/email/render.ex
Normal 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
|
||||
59
elixir/apps/domain/priv/static/emails/dark_mode.css
Normal file
59
elixir/apps/domain/priv/static/emails/dark_mode.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user