Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[
import_deps: [
:ash_oban,
:oban,
:ash_authentication_phoenix,
:ash_authentication,
:ash_postgres,
Expand Down
9 changes: 9 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
# General application configuration
import Config

config :ash_oban, pro?: false

config :helpcenter, Oban,
engine: Oban.Engines.Basic,
notifier: Oban.Notifiers.Postgres,
queues: [default: 10],
repo: Helpcenter.Repo,
plugins: [{Oban.Plugins.Cron, []}]

config :ash,
allow_forbidden_field_for_relationships_by_default?: true,
include_embedded_source_by_default?: false,
Expand Down
12 changes: 12 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,15 @@ config :phoenix_live_view,

# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false

config :helpcenter, Helpcenter.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: System.get_env("MAILTRAP_SERVER", "smtp.mailtrap.io"),
username: System.get_env("MAILTRAP_USERNAME", "76b2c94cffd164"),
password: System.get_env("MAILTRAP_PASSWORD", "ed60898722b1c7"),
ssl: false,
tls: :never,
auth: :always,
port: System.get_env("MAILTRAP_PORT", "2525"),
retries: System.get_env("MAILTRAP_RETRIES", "2"),
no_mx_lookups: System.get_env("MAILTRAP_NO_MX_LOOKUPS", "false")
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Config
config :helpcenter, Oban, testing: :manual
config :helpcenter, token_signing_secret: "eEDPOnkZl5Qv+3SbqpbbmRoZeyjgYKp6"
config :ash, disable_async?: true

Expand Down
14 changes: 4 additions & 10 deletions lib/helpcenter/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@ defmodule Helpcenter.Accounts do
use Ash.Domain, otp_app: :helpcenter

resources do
# Authentication
resource Helpcenter.Accounts.Token
resource Helpcenter.Accounts.User
resource Helpcenter.Accounts.Team
resource Helpcenter.Accounts.UserTeam
# the rest of the domain resources

# Authorization
resource Helpcenter.Accounts.Group
# Delete this resource Helpcenter.Accounts.Permission
resource Helpcenter.Accounts.GroupPermission
resource Helpcenter.Accounts.UserGroup
resource Helpcenter.Accounts.UserNotification do
define :notify, action: :create
end
end
end
128 changes: 128 additions & 0 deletions lib/helpcenter/accounts/user_notification.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# lib/helpcenter/accounts/user_notification.ex
defmodule Helpcenter.Accounts.UserNotification do
use Ash.Resource,
domain: Helpcenter.Accounts,
data_layer: AshPostgres.DataLayer,
# > Add Ash Oban extension for background jobs
extensions: [AshOban]

postgres do
table "user_notifications"
repo Helpcenter.Repo
end

# ================================================================
# Ash Oban configuration to add background jobs for your resource.
# ================================================================
oban do
triggers do
# > Ensure this trigger runs for all tenants.
list_tenants fn -> Helpcenter.Repo.all_tenants() end

trigger :send do
# Enable debug logging for testing
debug? true
# Specify the queue to use for this trigger
queue :default
# Action on this resource that is run when the trigger is invoked
action :send

trigger_once? true
# The action on this resource that is used to retrieve data to work with
# on this resource. In this case, we want to read unprocessed notifications.
# check in the actions block for the `unprocessed` action.
worker_read_action :unprocessed

# The worker module that will process the job automatically added for you by the Ash Oban extension.
# You can also specify a custom worker module if needed. It is based on action name
worker_module_name Helpcenter.Accounts.UserNotification.AshOban.Worker.Send
scheduler_module_name Helpcenter.Accounts.UserNotification.AshOban.Scheduler.Send
end
end
end

actions do
default_accept [:sender_user_id, :recipient_user_id, :subject, :body, :read_at, :status]
defaults [:read, :create, :update, :destroy]

update :send do
description "Send a new user notification to the user"
change Helpcenter.Accounts.UserNotification.Changes.DeliverEmail
end

read :unprocessed do
description "Read unprocessed notifications"
filter expr(processed == false)
prepare build(limit: 100, load: :recipient)
end
end

preparations do
prepare Helpcenter.Preparations.SetTenant
end

changes do
change Helpcenter.Changes.SetTenant
end

multitenancy do
strategy :context
end

attributes do
uuid_primary_key :id

attribute :sender_user_id, :uuid do
description "The user who sent the notification"
allow_nil? true
end

attribute :recipient_user_id, :uuid do
description "The user who received the notification"
allow_nil? false
end

attribute :subject, :string do
description "The subject of the notification"
allow_nil? false
end

attribute :body, :string do
description "The body of the notification"
allow_nil? false
end

attribute :read_at, :datetime do
description "The time a notification has been read"
default nil
allow_nil? true
end

attribute :status, :atom do
description "The status of the notification"
default :unread
allow_nil? false
constraints one_of: [:unread, :read, :archived]
end

attribute :processed, :boolean do
description "Whether the notification has been processed"
default false
allow_nil? false
end

timestamps()
end

relationships do
belongs_to :sender, Helpcenter.Accounts.User do
description "The user who sent the notification"
source_attribute :recipient_user_id
end

belongs_to :recipient, Helpcenter.Accounts.User do
description "The user who received the notification"
source_attribute :recipient_user_id
end
end
end
39 changes: 39 additions & 0 deletions lib/helpcenter/accounts/user_notification/changes/deliver_email.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# lib/helpcenter/accounts/user_notification/changes/deliver_email.ex
defmodule Helpcenter.Accounts.UserNotification.Changes.DeliverEmail do
use Ash.Resource.Change
import Swoosh.Email

def change(changeset, _opts, _context) do
changeset
|> Ash.Changeset.change_attribute(:processed, true)
|> Ash.Changeset.after_action(&deliver_email/2)
end

def atomic?(), do: true

def atomic(changeset, opts, context) do
{:ok, change(changeset, opts, context)}
end

defp deliver_email(_changeset, notification) do
%{recipient_user_id: user_id} = notification

# We need to know the recipient's email address
# so we can send the email. At this point
# We assume the recipient is an existing user
recipient =
Helpcenter.Accounts.User
|> Ash.get!(user_id, authorize?: false)

# Rely on Phoenix mailer infrastructure to send the email
new()
|> from({"noreply", "noreply@example.com"})
|> to(to_string(recipient.email))
|> subject(notification.subject)
|> text_body(notification.body)
|> html_body(notification.body)
|> Helpcenter.Mailer.deliver!()

{:ok, notification}
end
end
23 changes: 23 additions & 0 deletions lib/helpcenter/accounts/user_notification/changes/send_email.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Helpcenter.Accounts.UserNotification.Changes.SendEmail do
use Ash.Resource.Change
import Swoosh.Email

def change(changeset, _opts, _context) do
Ash.Changeset.after_action(changeset, &send_email/2)
end

def atomic(changeset, opts, context) do
{:ok, change(changeset, opts, context)}
end

defp send_email(_changeset, notification) do
new()
|> from({"noreply", "noreply@example.com"})
|> to("noreply@example.com")
|> subject(notification.subject || "New Notification")
|> html_body(notification.body)
|> Helpcenter.Mailer.deliver!()

{:ok, notification}
end
end
5 changes: 5 additions & 0 deletions lib/helpcenter/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ defmodule Helpcenter.Application do
HelpcenterWeb.Telemetry,
Helpcenter.Repo,
{DNSCluster, query: Application.get_env(:helpcenter, :dns_cluster_query) || :ignore},
{Oban,
AshOban.config(
Application.fetch_env!(:helpcenter, :ash_domains),
Application.fetch_env!(:helpcenter, Oban)
)},
{Phoenix.PubSub, name: Helpcenter.PubSub},
# Start the Finch HTTP client for sending emails
{Finch, name: Helpcenter.Finch},
Expand Down
4 changes: 4 additions & 0 deletions lib/helpcenter/changes/set_tenant.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ defmodule Helpcenter.Changes.SetTenant do
end

def change(changeset, _opts, _context), do: changeset

def atomic(changeset, opts, context) do
{:ok, change(changeset, opts, context)}
end
end
3 changes: 3 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ defmodule Helpcenter.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
{:gen_smtp, "~> 1.0"},
{:oban, "~> 2.0"},
{:ash_oban, "~> 0.4"},
{:bcrypt_elixir, "~> 3.0"},
{:ash_authentication, "~> 4.0"},
{:ash_authentication_phoenix, "~> 2.0"},
Expand Down
Loading
Loading