Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ npm-debug.log
.devbox
.bundle
.env
.claude
8 changes: 8 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } })

liveSocket.connect()
window.liveSocket = liveSocket
16 changes: 14 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ config :ex_gridhook,
# Configures the endpoint
config :ex_gridhook, ExGridhookWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: System.get_env("SECRET_KEY_BASE") || "no secret",
pubsub_server: ExGridhook.PubSub
secret_key_base:
System.get_env("SECRET_KEY_BASE") ||
"dev_secret_key_base_at_least_64_bytes_long_do_not_use_in_production_xxxxxxxxxxx",
pubsub_server: ExGridhook.PubSub,
live_view: [signing_salt: "gridhook_lv"]

config :ex_gridhook,
revision: {:system, "HEROKU_SLUG_COMMIT", "some revision"}
Expand All @@ -39,6 +42,15 @@ config :honeybadger,
origin: System.get_env("HONEYBADGER_ORIGIN", "https://api.honeybadger.io"),
breadcrumbs_enabled: true

config :esbuild,
version: "0.17.11",
default: [
args:
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
8 changes: 6 additions & 2 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ config :ex_gridhook, ExGridhookWeb.Endpoint,
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: []
watchers: [
esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
]

# ## SSL Support
#
Expand All @@ -33,7 +35,9 @@ config :ex_gridhook, ExGridhookWeb.Endpoint,
config :ex_gridhook, ExGridhookWeb.Endpoint,
live_reload: [
patterns: [
~r{priv/gettext/.*(po)$}
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
~r{priv/gettext/.*(po)$},
~r{lib/ex_gridhook_web/(live|components|controllers)/.*(ex|heex)$}
]
]

Expand Down
4 changes: 4 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ if config_env() == :prod do

config :logger, level: System.get_env("LOG_LEVEL") || :info

config :ex_gridhook,
sso_secret_key: System.get_env("SSO_SECRET_KEY"),
sso_request_url: System.get_env("SSO_REQUEST_URL")

# Configure the database
config :ex_gridhook, ExGridhook.Repo,
adapter: Ecto.Adapters.Postgres,
Expand Down
84 changes: 84 additions & 0 deletions lib/ex_gridhook/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,28 @@ defmodule ExGridhook.Event do
@moduledoc false

use Ecto.Schema
import Ecto.Query
alias ExGridhook.Event
alias ExGridhook.EventsData
alias ExGridhook.Repo
alias Ecto.Multi
@timestamps_opts [type: :utc_datetime]

@event_types ~w(processed dropped delivered deferred bounce open click spamreport unsubscribe)

@event_descriptions %{
"processed" => "Message has been received and is ready to be delivered.",
"dropped" =>
"You may see the following drop reasons: invalid SMTPAPI header, spam content (if spam checker app enabled), unsubscribed address, bounced address, spam reporting address, invalid.",
"delivered" => "Message has been successfully delivered to the receiving server.",
"deferred" => "Recipient's email server temporarily rejected message.",
"bounce" => "Receiving server could not or would not accept message.",
"open" => "Recipient has opened the HTML message.",
"click" => "Recipient clicked on a link within the message.",
"spamreport" => "Recipient marked message as spam.",
"unsubscribe" => "Recipient clicked on message's subscription management link."
}

schema "events" do
field(:email, :string)
field(:name, :string)
Expand All @@ -23,6 +39,74 @@ defmodule ExGridhook.Event do
timestamps(inserted_at: :created_at)
end

def event_types, do: @event_types
def event_description(name), do: Map.get(@event_descriptions, name, "No description")

def total_events, do: EventsData.total_events()

def newest_time do
Repo.one(from e in __MODULE__, select: max(e.happened_at))
end

def oldest_time do
Repo.one(from e in __MODULE__, select: min(e.happened_at))
end

# Uses a recursive SQL query to efficiently find distinct mailer actions on large tables.
# See: http://zogovic.com/post/44856908222/optimizing-postgresql-query-for-distinct-values
def mailer_actions do
sql = """
WITH RECURSIVE t(n) AS (
SELECT MIN(mailer_action) FROM events
UNION
SELECT (SELECT mailer_action FROM events WHERE mailer_action > n ORDER BY mailer_action LIMIT 1)
FROM t WHERE n IS NOT NULL
)
SELECT n FROM t;
"""

%{rows: rows} = Repo.query!(sql)
rows |> List.flatten() |> Enum.reject(&is_nil/1)
end

def query_by_user_identifier(user_identifier) do
from(e in __MODULE__, where: e.user_identifier == ^user_identifier)
end

def with_email_if_present(query, email) when email in [nil, ""], do: query

def with_email_if_present(query, email) do
downcased = String.downcase(email)
from(e in query, where: e.email == ^downcased)
end

def with_name_if_present(query, name) when name in [nil, ""], do: query
def with_name_if_present(query, name), do: from(e in query, where: e.name == ^name)

def with_mailer_action_if_present(query, action) when action in [nil, ""], do: query

def with_mailer_action_if_present(query, action),
do: from(e in query, where: e.mailer_action == ^action)

def with_associated_record_if_present(query, record) when record in [nil, ""], do: query

def with_associated_record_if_present(query, record) do
from(e in query, where: fragment("? @> ?", e.associated_records, ^[record]))
end

def recent_first(query), do: from(e in query, order_by: [desc: e.happened_at, desc: e.id])

def paginate(query, page, per) do
from(e in query, limit: ^per, offset: ^((page - 1) * per))
end

def remove_by_email(email) do
downcased = String.downcase(email)
{count, _} = Repo.delete_all(from e in __MODULE__, where: e.email == ^downcased)
EventsData.decrement(count)
count
end

def create_event_data(attributes \\ %{}) do
category = Map.get(attributes, "category")
event = Map.get(attributes, "event")
Expand Down
14 changes: 14 additions & 0 deletions lib/ex_gridhook/events_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,24 @@ defmodule ExGridhook.EventsData do
@moduledoc false

use Ecto.Schema
import Ecto.Query
alias ExGridhook.Repo

schema "events_data" do
field(:total_events, :integer)

timestamps()
end

def total_events do
Repo.one(from e in __MODULE__, select: e.total_events, limit: 1) || 0
end

def increment(count) do
Repo.update_all(__MODULE__, inc: [total_events: count])
end

def decrement(count) do
Repo.update_all(__MODULE__, inc: [total_events: -count])
end
end
44 changes: 42 additions & 2 deletions lib/ex_gridhook_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule ExGridhookWeb do
This can be used in your application as:

use ExGridhookWeb, :controller
use ExGridhookWeb, :view
use ExGridhookWeb, :live_view

The definitions below will be executed for every view,
controller, etc, so keep them short and clean, focused
Expand All @@ -17,7 +17,7 @@ defmodule ExGridhookWeb do
and import those modules here.
"""

def static_paths, do: ~w(css fonts images js favicon.ico robots.txt)
def static_paths, do: ~w(assets events dinomail.gif favicon.ico robots.txt)

def controller do
quote do
Expand All @@ -30,6 +30,45 @@ defmodule ExGridhookWeb do
end
end

def live_view do
quote do
use Phoenix.LiveView, layout: {ExGridhookWeb.Layouts, :app}

unquote(html_helpers())
end
end

def live_component do
quote do
use Phoenix.LiveComponent

unquote(html_helpers())
end
end

def html do
quote do
use Phoenix.Component

import Phoenix.Controller,
only: [get_csrf_token: 0, view_module: 1, view_template: 1]

unquote(html_helpers())
end
end

defp html_helpers do
quote do
import Phoenix.HTML
use PhoenixHTMLHelpers

import Phoenix.LiveView.Helpers
import ExGridhookWeb.CoreComponents

unquote(verified_routes())
end
end

def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
Expand All @@ -45,6 +84,7 @@ defmodule ExGridhookWeb do
import Plug.Conn
import Plug.BasicAuth
import Phoenix.Controller
import Phoenix.LiveView.Router
end
end

Expand Down
24 changes: 24 additions & 0 deletions lib/ex_gridhook_web/auth/jwt_plug.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule ExGridhookWeb.Auth.JwtPlug do
@moduledoc """
Thin wrapper around AuctionetSingleSignOnPlug that reads config from
application env. Skips authentication in dev when SSO_SECRET_KEY is not set.
"""

def init(opts), do: opts

def call(conn, _opts) do
secret_key = Application.get_env(:ex_gridhook, :sso_secret_key)

if is_nil(secret_key) do
conn
else
opts =
AuctionetSingleSignOnPlug.init(
sso_secret_key: secret_key,
sso_request_url: Application.get_env(:ex_gridhook, :sso_request_url)
)

AuctionetSingleSignOnPlug.call(conn, opts)
end
end
end
4 changes: 4 additions & 0 deletions lib/ex_gridhook_web/components/core_components.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
defmodule ExGridhookWeb.CoreComponents do
@moduledoc false
use Phoenix.Component
end
5 changes: 5 additions & 0 deletions lib/ex_gridhook_web/components/layouts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule ExGridhookWeb.Layouts do
use ExGridhookWeb, :html

embed_templates "layouts/*"
end
14 changes: 14 additions & 0 deletions lib/ex_gridhook_web/components/layouts/app.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div class="content">
<header>
<h1 class="main-title"><a href="/">Gridlook</a></h1>
</header>
<%= @inner_content %>
<footer>
<img class="dinomail" src={~p"/dinomail.gif"} alt="" />
<p>
<a href="http://github.com/barsoom/gridlook">Gridlook</a>
by <a href="http://barsoom.se">Barsoom</a>.
</p>
</footer>
</div>
<script src={~p"/assets/app.js"}></script>
15 changes: 15 additions & 0 deletions lib/ex_gridhook_web/components/layouts/root.html.heex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<title>Gridlook</title>
<link rel="stylesheet" href={~p"/assets/app.css"} />
<link rel="icon" type="image/x-icon" href={~p"/favicon.ico"} />
</head>
<body>
<%= Phoenix.HTML.raw(System.get_env("CUSTOM_HTML_HEADER", "")) %>
<%= @inner_content %>
</body>
</html>
Loading
Loading