feat: mastodon accounts

This commit is contained in:
sloane 2025-04-28 08:20:00 -04:00
parent fcffea0ef7
commit 96e778ff91
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
18 changed files with 215 additions and 3 deletions

View file

@ -2,10 +2,13 @@
# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either
# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git.
SECRETS=$(kamal secrets fetch --adapter 1password --account Perrault --from Private/sloanelybutsurely.com KAMAL_REGISTRY_PASSWORD POSTGRES_PASSWORD SECRET_KEY_BASE)
SECRETS=$(kamal secrets fetch --adapter 1password --account Perrault --from Private/sloanelybutsurely.com KAMAL_REGISTRY_PASSWORD POSTGRES_PASSWORD SECRET_KEY_BASE MASTODON_CLIENT_ID MASTODON_CLIENT_SECRET)
KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS)
POSTGRES_PASSWORD=$(kamal secrets extract POSTGRES_PASSWORD $SECRETS)
SECRET_KEY_BASE=$(kamal secrets extract SECRET_KEY_BASE $SECRETS)
DATABASE_URL="postgresql://sloanely_but_surely_prod:$POSTGRES_PASSWORD@sloanelybutsurely-db:5432/sloanely_but_surely_prod"
MASTODON_CLIENT_ID=$(kamal secrets extract MASTODON_CLIENT_ID $SECRETS)
MASTODON_CLIENT_SECRET=$(kamal secrets extract MASTODON_CLIENT_SECRET $SECRETS)

View file

@ -59,4 +59,18 @@ config :tailwind,
config :flop, repo: Core.Repo
config :tesla, adapter: Tesla.Adapter.Mint
config :ueberauth, Ueberauth,
providers: [
mastodon:
{Ueberauth.Strategy.Mastodon,
[
instance: "https://tech.lgbt",
client_id: {System, :get_env, ["MASTODON_CLIENT_ID"]},
client_secret: {System, :get_env, ["MASTODON_CLIENT_SECRET"]},
scope: "read write push"
]}
]
import_config "#{config_env()}.exs"

View file

@ -12,6 +12,8 @@ env:
secret:
- DATABASE_URL
- SECRET_KEY_BASE
- MASTODON_CLIENT_ID
- MASTODON_CLIENT_SECRET
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.

View file

@ -1,4 +1,4 @@
defmodule Core do
@moduledoc false
use Boundary, deps: [Schema], exports: [Accounts, Posts, Author, DateTime, Release]
use Boundary, deps: [Schema], exports: [Accounts, Posts, Author, DateTime, Release, Syndication]
end

14
lib/core/syndication.ex Normal file
View file

@ -0,0 +1,14 @@
defmodule Core.Syndication do
alias __MODULE__
def get_mastodon_account(user) do
Core.Repo.get_by(Schema.MastodonAccount, user_id: user.id)
end
def save_mastodon_account(user, attrs) do
user
|> Ecto.build_assoc(:mastodon_account)
|> Syndication.MastodonAccount.changeset(attrs)
|> Core.Repo.insert()
end
end

View file

@ -0,0 +1,11 @@
defmodule Core.Syndication.MastodonAccount do
import Ecto.Changeset
def changeset(%Schema.MastodonAccount{} = mastodon_account, attrs) do
mastodon_account
|> cast(attrs, [:uid, :access_token])
|> validate_required([:uid, :access_token])
|> unique_constraint(:user_id)
|> unique_constraint(:uid)
end
end

View file

@ -1,6 +1,6 @@
defmodule Schema do
@moduledoc false
use Boundary, deps: [], exports: [Post, User, UserToken]
use Boundary, deps: [], exports: [Post, User, UserToken, MastodonAccount]
defmacro __using__(_) do
quote do

View file

@ -0,0 +1,12 @@
defmodule Schema.MastodonAccount do
use Schema
schema "mastodon_accounts" do
field :uid, :string
field :access_token, :string, redact: true
belongs_to :user, Schema.User
timestamps()
end
end

View file

@ -8,6 +8,8 @@ defmodule Schema.User do
field :hashed_password, :string, redact: true
field :current_password, :string, virtual: true, redact: true
has_one :mastodon_account, Schema.MastodonAccount
timestamps(type: :utc_datetime_usec)
end
end

View file

@ -0,0 +1,26 @@
defmodule Web.AuthController do
use Web, :controller
plug Ueberauth
def callback(
%{assigns: %{ueberauth_auth: %{provider: :mastodon} = auth, current_user: user}} = conn,
_params
) do
{:ok, _mastodon_account} =
Core.Syndication.save_mastodon_account(user, %{
uid: auth.uid,
access_token: auth.credentials.token
})
conn
|> put_flash(:info, "Mastodon account registered")
|> redirect(to: ~p"/admin/syndication")
end
def callback(conn, _params) do
conn
|> put_flash(:error, "Mastodon auth failure")
|> redirect(to: ~p"/admin/syndication")
end
end

View file

@ -12,6 +12,11 @@
microblog
</.link>
</li>
<li>
<.link navigate={~p"/admin/syndication"}>
syndication
</.link>
</li>
</ul>
</nav>
</header>

View file

@ -0,0 +1,13 @@
defmodule Web.AdminSyndicationLive do
use Web, :live_view
def mount(_params, _session, socket) do
mastodon_account = Core.Syndication.get_mastodon_account(socket.assigns.current_user)
socket =
socket
|> assign(:mastodon_account, mastodon_account)
{:ok, socket}
end
end

View file

@ -0,0 +1,37 @@
<div class="flex flex-col py-4 px-6">
<header class="mb-4">
<nav>
<ul class="flex flex-row gap-x-4">
<li>
<.link navigate={~p"/admin/writing"}>
writing
</.link>
</li>
<li>
<.link navigate={~p"/admin/microblog"}>
microblog
</.link>
</li>
<li>
<.link class="underline" patch={~p"/admin/syndication"}>
syndication
</.link>
</li>
</ul>
</nav>
</header>
<main class="flex flex-col">
<div>
<strong>Mastodon: </strong>
<%= if @mastodon_account do %>
<.link href={@mastodon_account.uid} target="_blank">{@mastodon_account.uid}</.link>
<% else %>
<.link href={~p"/auth/mastodon"}>Connect account</.link>
<% end %>
</div>
<div>
<strong>Bluesky: </strong>Coming soon!
</div>
</main>
</div>

View file

@ -25,6 +25,13 @@ defmodule Web.Router do
post "/admin/users/log_in", UserSessionController, :create
end
scope "/auth", Web do
pipe_through [:browser, :require_authenticated_user]
get "/mastodon", AuthController, :request
get "/mastodon/callback", AuthController, :callback
end
scope "/admin", Web do
pipe_through [:browser, :require_authenticated_user]
@ -36,6 +43,8 @@ defmodule Web.Router do
live "/posts/new", AdminPostLive, :new
live "/posts/:post_id", AdminPostLive, :edit
live "/syndication", AdminSyndicationLive, :index
end
end

View file

@ -66,6 +66,10 @@ defmodule SlaonelyButSurely.MixProject do
{:flop_phoenix, "~> 0.24.1"},
{:oban, "~> 2.19"},
{:igniter, "~> 0.5", only: [:dev]},
{:ueberauth, "~> 0.10"},
{:ueberauth_mastodon, "~> 0.3.0"},
{:tesla, "~> 1.14"},
{:mint, "~> 1.7"},
# Added dev and/or test dependencies
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},

View file

@ -31,6 +31,7 @@
"igniter": {:hex, :igniter, "0.5.47", "7a1041d5e38303e526fa6b6de37c9e78013f5cb573833ed51183d18e3a152f10", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "53a900909e20f217a25d15a34fef629c562b4822c1fb39cfa5d6999bc72992ed"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"mastodon_client": {:hex, :mastodon_client, "0.1.0", "7f1a9e54367d0e126c76d0bb1097de346fb9c6da6d682a6b57bc936c92d643eb", [:mix], [{:hackney, "~> 1.18", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:tesla, "~> 1.4", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "3daa37fc7a95430eb0b77cdb168af58b8db84efea6e0f3244d15d45f39a87160"},
"mdex": {:hex, :mdex, "0.5.0", "252c83cebc6a089801dfc1e142b4d98c9c358378ec7096a94796bce8bd13b0fc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "73e3ddee03130267e3be6aaf47a7f423c6f86add4bb5c62b352465cd9fb87d95"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
@ -65,12 +66,15 @@
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"tesla": {:hex, :tesla, "1.14.1", "71c5b031b4e089c0fbfb2b362e24b4478465773ae4ef569760a8c2899ad1e73c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c1dde8140a49a3bef5bb622356e77ac5a24ad0c8091f12c3b7fc1077ce797155"},
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
"thousand_island": {:hex, :thousand_island, "1.3.10", "a9971ebab1dfb36e2710a86b37c3f54973fbc9470d892035334415521fb53328", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17ab1f1b13aadb1f4b4c8e5b59c06874d701119fed082884c9c6d38addad254f"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
"tzdata": {:hex, :tzdata, "1.1.2", "45e5f1fcf8729525ec27c65e163be5b3d247ab1702581a94674e008413eef50b", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cec7b286e608371602318c414f344941d5eb0375e14cfdab605cca2fe66cba8b"},
"ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"},
"ueberauth_mastodon": {:hex, :ueberauth_mastodon, "0.3.0", "e0b80adc29d8734f74cac908d25e2113739112ea84b95f3c9f27ef3161128426", [:mix], [{:hackney, "~> 1.20", [hex: :hackney, repo: "hexpm", optional: true]}, {:mastodon_client, "~> 0.1", [hex: :mastodon_client, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.10.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "28a049497b3b708a232c8d6da1c7fb80c92ae4a6961f2c9274dfe9333a1aa7d0"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},

View file

@ -0,0 +1,17 @@
defmodule Core.Repo.Migrations.CreateMastodonAuthTables do
use Ecto.Migration
def change do
create table(:mastodon_accounts, primary_key: false) do
add :id, :uuid, primary_key: true
add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false
add :uid, :text, null: false
add :access_token, :text, null: false
timestamps(type: :utc_datetime_usec)
end
create unique_index(:mastodon_accounts, [:user_id])
create unique_index(:mastodon_accounts, [:uid])
end
end

View file

@ -0,0 +1,39 @@
#!/usr/bin/env elixir
Mix.install([
{:req, "~> 0.5.10"}
])
{opts, []} =
OptionParser.parse!(System.argv(),
strict: [
instance: :string,
client_name: :string,
redirect_uri: :string,
scopes: :string,
website: :string
]
)
instance = Keyword.fetch!(opts, :instance)
client_name = Keyword.get(opts, :client_name, "sloanelybutsurely.com")
redirect_uri =
Keyword.get(opts, :redirect_uri, "https://sloanelybutsurely.com/auth/mastodon/callback")
scopes = Keyword.get(opts, :scopes, "read write push")
website = Keyword.get(opts, :website, "https://sloanelybutsurely.com")
%{status: 200, body: resp} =
Req.post!(
base_url: instance,
url: "/api/v1/apps",
json: %{
client_name: client_name,
redirect_uris: [redirect_uri],
scopes: scopes,
website: website
}
)
IO.inspect(resp)