Packages

phoenix_kit

1.7.202
1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules maintenance web plugs maintenance_mode.ex
Raw

lib/modules/maintenance/web/plugs/maintenance_mode.ex

defmodule PhoenixKitWeb.Plugs.MaintenanceMode do
@moduledoc """
Plug that enforces maintenance mode for non-admin users on controller routes.
LiveView routes are handled by the on_mount hook in `auth.ex` which overrides
the layout instead of redirecting. This plug handles the remaining non-LiveView
routes (POST actions, OAuth callbacks, etc.) by rendering a 503 maintenance page.
Adds a `Retry-After` header when a scheduled end time is known.
"""
import Plug.Conn
alias PhoenixKit.Modules.Maintenance
alias PhoenixKit.Users.Auth
alias PhoenixKit.Users.Auth.Scope
def init(opts), do: opts
def call(conn, _opts) do
# Clean up stale state if the scheduled end time has passed
Maintenance.cleanup_expired_schedule()
if Maintenance.active?() do
handle_maintenance_mode(conn)
else
conn
end
end
defp handle_maintenance_mode(conn) do
if should_skip?(conn.request_path) do
conn
else
user = get_user_from_session(conn)
if admin_or_owner?(user) do
conn
else
render_maintenance_page(conn)
end
end
end
defp should_skip?(path) do
static_asset?(path) or auth_route?(path)
end
defp static_asset?(path) do
String.starts_with?(path, "/assets/") or
String.starts_with?(path, "/images/") or
String.starts_with?(path, "/fonts/") or
String.starts_with?(path, "/favicon")
end
defp auth_route?(path) do
url_prefix = PhoenixKit.Config.get_url_prefix()
prefix_path = fn route ->
case url_prefix do
"" -> route
"/" -> route
prefix -> prefix <> route
end
end
auth_routes = [
"/users/log-in",
"/users/reset-password",
"/users/confirm",
"/users/magic-link",
"/users/auth/"
]
# Use starts_with? (not contains?) to prevent parent-app paths like
# "/blog/users/log-in-to-us" from bypassing maintenance mode.
Enum.any?(auth_routes, fn route ->
String.starts_with?(path, prefix_path.(route)) or
String.starts_with?(path, route)
end)
end
defp admin_or_owner?(nil), do: false
defp admin_or_owner?(user) do
scope = Scope.for_user(user)
Scope.admin?(scope) || Scope.owner?(scope)
end
defp get_user_from_session(conn) do
if user_token = get_session(conn, :user_token) do
Auth.get_user_by_session_token(user_token)
else
nil
end
end
defp render_maintenance_page(conn) do
config = Maintenance.get_config()
header = Phoenix.HTML.html_escape(config.header) |> Phoenix.HTML.safe_to_string()
subtext = Phoenix.HTML.html_escape(config.subtext) |> Phoenix.HTML.safe_to_string()
# Inline styles so the page works without the parent app's asset pipeline.
# Controller routes served by this plug may not have access to the digested
# app.css, so we ship a self-contained page.
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>#{header}</title>
<meta http-equiv="refresh" content="5" />
<style>
html, body { margin: 0; padding: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }
body { background: #f2f2f2; color: #1a1a1a; }
.wrap { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 1rem; box-sizing: border-box; }
.card { background: #fff; border: 2px dashed #d4d4d4; border-radius: 1rem; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); max-width: 42rem; width: 100%; padding: 3rem 1.5rem; text-align: center; box-sizing: border-box; }
.icon { font-size: 5rem; margin-bottom: 1.5rem; opacity: 0.7; }
h1 { font-size: 3rem; font-weight: 700; margin: 0 0 1.5rem 0; line-height: 1.1; }
p { font-size: 1.25rem; line-height: 1.6; opacity: 0.7; margin: 0; }
@media (prefers-color-scheme: dark) {
body { background: #1d232a; color: #a6adbb; }
.card { background: #191e24; border-color: #2a323c; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<div class="icon">🚧</div>
<h1>#{header}</h1>
<p>#{subtext}</p>
</div>
</div>
</body>
</html>
"""
conn
|> maybe_add_retry_after()
|> put_resp_content_type("text/html")
|> send_resp(:service_unavailable, html)
|> halt()
end
defp maybe_add_retry_after(conn) do
case Maintenance.seconds_until_end() do
seconds when is_integer(seconds) and seconds > 0 ->
put_resp_header(conn, "retry-after", Integer.to_string(seconds))
_ ->
conn
end
end
end