diff --git a/config/config.exs b/config/config.exs index f633469..cd278bf 100644 --- a/config/config.exs +++ b/config/config.exs @@ -126,7 +126,17 @@ config :esbuild, args: ~w(js/index.tsx js/app.js --bundle --target=es2022 --outdir=../priv/static/assets --external:/fonts/* --external:/images/* --alias:@=. --splitting --format=esm), cd: Path.expand("../assets", __DIR__), - env: %{"NODE_PATH" => Enum.join([Path.expand("../deps", __DIR__), Path.expand(Mix.Project.build_path()), Path.expand("../_build/dev", __DIR__)], ":")} + env: %{ + "NODE_PATH" => + Enum.join( + [ + Path.expand("../deps", __DIR__), + Path.expand(Mix.Project.build_path()), + Path.expand("../_build/dev", __DIR__) + ], + ":" + ) + } ] # Configure tailwind (the version is required) diff --git a/lib/mixer/accounts.ex b/lib/mixer/accounts.ex index a40288d..c036cb3 100644 --- a/lib/mixer/accounts.ex +++ b/lib/mixer/accounts.ex @@ -1,6 +1,18 @@ defmodule Mixer.Accounts do use Ash.Domain, otp_app: :mixer, extensions: [AshTypescript.Rpc, AshAdmin.Domain] + typescript_rpc do + resource Mixer.Accounts.User do + rpc_action :read_user, :read + end + + resource Mixer.Accounts.Follow do + rpc_action :read_follow, :read + rpc_action :follow_user, :follow + rpc_action :unfollow_user, :unfollow + end + end + admin do show? true end @@ -12,15 +24,4 @@ defmodule Mixer.Accounts do resource Mixer.Accounts.Follow end - - typescript_rpc do - resource Mixer.Accounts.User do - rpc_action :read_user, :read - end - resource Mixer.Accounts.Follow do - rpc_action :read_follow, :read - rpc_action :follow_user, :follow - rpc_action :unfollow_user, :unfollow - end - end end diff --git a/lib/mixer/accounts/follow.ex b/lib/mixer/accounts/follow.ex index c599ef6..b9122d7 100644 --- a/lib/mixer/accounts/follow.ex +++ b/lib/mixer/accounts/follow.ex @@ -1,5 +1,6 @@ defmodule Mixer.Accounts.Follow do require Ash.Query + use Ash.Resource, domain: Mixer.Accounts, data_layer: AshPostgres.DataLayer, @@ -20,25 +21,6 @@ defmodule Mixer.Accounts.Follow do type_name "follows" end - attributes do - uuid_primary_key :id - create_timestamp :created_at - end - - relationships do - belongs_to :follower, Mixer.Accounts.User do - primary_key? true - allow_nil? false - attribute_writable? true - end - - belongs_to :following, Mixer.Accounts.User do - primary_key? true - allow_nil? false - attribute_writable? true - end - end - actions do defaults [:read, :destroy] @@ -48,6 +30,7 @@ defmodule Mixer.Accounts.Follow do upsert_identity :unique_follow accept [:following_id] change relate_actor(:follower) + validate fn changeset, _context -> follower_id = Ash.Changeset.get_attribute(changeset, :follower_id) following_id = Ash.Changeset.get_attribute(changeset, :following_id) @@ -82,10 +65,6 @@ defmodule Mixer.Accounts.Follow do end end - identities do - identity :unique_follow, [:follower_id, :following_id] - end - policies do policy action_type(:read) do authorize_if always() @@ -99,4 +78,27 @@ defmodule Mixer.Accounts.Follow do authorize_if actor_present() end end + + attributes do + uuid_primary_key :id + create_timestamp :created_at + end + + relationships do + belongs_to :follower, Mixer.Accounts.User do + primary_key? true + allow_nil? false + attribute_writable? true + end + + belongs_to :following, Mixer.Accounts.User do + primary_key? true + allow_nil? false + attribute_writable? true + end + end + + identities do + identity :unique_follow, [:follower_id, :following_id] + end end diff --git a/lib/mixer/accounts/user/senders/send_magic_link_email.ex b/lib/mixer/accounts/user/senders/send_magic_link_email.ex index 1eec038..08d75bf 100644 --- a/lib/mixer/accounts/user/senders/send_magic_link_email.ex +++ b/lib/mixer/accounts/user/senders/send_magic_link_email.ex @@ -31,14 +31,21 @@ defmodule Mixer.Accounts.User.Senders.SendMagicLinkEmail do defp body(params) do # NOTE: You may have to change this to match your magic link acceptance URL. link = url(~p"/magic_link/#{params[:token]}") - email_template("Your magic link", "Hello, #{params[:email]}!", """ -
- Use the button below to sign in to Mixer. This link is valid for a short time and can only be used once. -
-- If you didn't request this, you can safely ignore this email. -
- """, link, "Sign In to Mixer") + + email_template( + "Your magic link", + "Hello, #{params[:email]}!", + """ ++ Use the button below to sign in to Mixer. This link is valid for a short time and can only be used once. +
++ If you didn't request this, you can safely ignore this email. +
+ """, + link, + "Sign In to Mixer" + ) end defp email_template(title, greeting, content, button_url, button_label) do diff --git a/lib/mixer/accounts/user/senders/send_new_user_confirmation_email.ex b/lib/mixer/accounts/user/senders/send_new_user_confirmation_email.ex index 128e367..531e750 100644 --- a/lib/mixer/accounts/user/senders/send_new_user_confirmation_email.ex +++ b/lib/mixer/accounts/user/senders/send_new_user_confirmation_email.ex @@ -22,14 +22,21 @@ defmodule Mixer.Accounts.User.Senders.SendNewUserConfirmationEmail do defp body(params) do link = url(~p"/confirm_new_user/#{params[:token]}") - email_template("Confirm your email", "Welcome to Mixer!", """ -- Thanks for signing up. Just one more step — confirm your email address to activate your account. -
-- If you didn't create an account on Mixer, you can safely ignore this email. -
- """, link, "Confirm Email Address") + + email_template( + "Confirm your email", + "Welcome to Mixer!", + """ ++ Thanks for signing up. Just one more step — confirm your email address to activate your account. +
++ If you didn't create an account on Mixer, you can safely ignore this email. +
+ """, + link, + "Confirm Email Address" + ) end defp email_template(title, greeting, content, button_url, button_label) do diff --git a/lib/mixer/accounts/user/senders/send_password_reset_email.ex b/lib/mixer/accounts/user/senders/send_password_reset_email.ex index 3d94c56..ded45b6 100644 --- a/lib/mixer/accounts/user/senders/send_password_reset_email.ex +++ b/lib/mixer/accounts/user/senders/send_password_reset_email.ex @@ -22,14 +22,21 @@ defmodule Mixer.Accounts.User.Senders.SendPasswordResetEmail do defp body(params) do link = url(~p"/password-reset/#{params[:token]}") - email_template("Reset your password", "Password reset request", """ -- We received a request to reset the password for your Mixer account. Click the button below to choose a new one. -
-- If you didn't request a password reset, you can safely ignore this email — your password will not change. -
- """, link, "Reset My Password") + + email_template( + "Reset your password", + "Password reset request", + """ ++ We received a request to reset the password for your Mixer account. Click the button below to choose a new one. +
++ If you didn't request a password reset, you can safely ignore this email — your password will not change. +
+ """, + link, + "Reset My Password" + ) end defp email_template(title, greeting, content, button_url, button_label) do diff --git a/lib/mixer/posts.ex b/lib/mixer/posts.ex index 4554943..93643ef 100644 --- a/lib/mixer/posts.ex +++ b/lib/mixer/posts.ex @@ -3,16 +3,6 @@ defmodule Mixer.Posts do otp_app: :mixer, extensions: [AshTypescript.Rpc, AshAdmin.Domain] - admin do - show? true - end - - resources do - resource Mixer.Posts.Tweet - resource Mixer.Posts.TweetLike - resource Mixer.Posts.Media - end - typescript_rpc do resource Mixer.Posts.Tweet do rpc_action :create_tweet, :create @@ -27,4 +17,14 @@ defmodule Mixer.Posts do rpc_action :read_media, :read end end + + admin do + show? true + end + + resources do + resource Mixer.Posts.Tweet + resource Mixer.Posts.TweetLike + resource Mixer.Posts.Media + end end diff --git a/lib/mixer/posts/media.ex b/lib/mixer/posts/media.ex index 0886fd5..6989f16 100644 --- a/lib/mixer/posts/media.ex +++ b/lib/mixer/posts/media.ex @@ -38,6 +38,24 @@ defmodule Mixer.Posts.Media do end end + policies do + policy action_type(:read) do + authorize_if always() + end + + policy action(:upload) do + authorize_if actor_present() + end + + policy action(:link_to_tweet) do + authorize_if relates_to_actor_via(:user) + end + + policy action_type(:destroy) do + authorize_if relates_to_actor_via(:user) + end + end + attributes do uuid_primary_key :id @@ -64,22 +82,4 @@ defmodule Mixer.Posts.Media do public? true end end - - policies do - policy action_type(:read) do - authorize_if always() - end - - policy action(:upload) do - authorize_if actor_present() - end - - policy action(:link_to_tweet) do - authorize_if relates_to_actor_via(:user) - end - - policy action_type(:destroy) do - authorize_if relates_to_actor_via(:user) - end - end end diff --git a/lib/mixer/posts/media_uploader.ex b/lib/mixer/posts/media_uploader.ex index 64efb94..ee147b8 100644 --- a/lib/mixer/posts/media_uploader.ex +++ b/lib/mixer/posts/media_uploader.ex @@ -10,7 +10,8 @@ defmodule Mixer.Posts.MediaUploader do if ext in @extensions, do: :ok, else: {:error, "unsupported file type #{ext}"} end - def storage_dir(_version, {_file, scope}), do: "uploads/media/#{scope.user_id}/#{scope.media_id}" + def storage_dir(_version, {_file, scope}), + do: "uploads/media/#{scope.user_id}/#{scope.media_id}" def filename(_version, {file, _scope}) do Path.basename(file.file_name, Path.extname(file.file_name)) diff --git a/lib/mixer/posts/tweet.ex b/lib/mixer/posts/tweet.ex index 345270b..6153901 100644 --- a/lib/mixer/posts/tweet.ex +++ b/lib/mixer/posts/tweet.ex @@ -14,10 +14,6 @@ defmodule Mixer.Posts.Tweet do repo Mixer.Repo end - typescript do - type_name "tweets" - end - state_machine do initial_states [:drafted, :posted] default_initial_state :drafted @@ -27,6 +23,10 @@ defmodule Mixer.Posts.Tweet do end end + typescript do + type_name "tweets" + end + actions do defaults [:read, :destroy] @@ -36,6 +36,7 @@ defmodule Mixer.Posts.Tweet do argument :media_id, :uuid, allow_nil?: true change relate_actor(:user) change transition_state(:posted) + change fn changeset, context -> case Ash.Changeset.get_argument(changeset, :media_id) do nil -> @@ -45,7 +46,9 @@ defmodule Mixer.Posts.Tweet do Ash.Changeset.after_action(changeset, fn _changeset, tweet -> Mixer.Posts.Media |> Ash.get!(media_id, authorize?: false) - |> Ash.Changeset.for_update(:link_to_tweet, %{tweet_id: tweet.id}, actor: context.actor) + |> Ash.Changeset.for_update(:link_to_tweet, %{tweet_id: tweet.id}, + actor: context.actor + ) |> Ash.update!() {:ok, tweet} @@ -111,6 +114,32 @@ defmodule Mixer.Posts.Tweet do end end + policies do + policy action_type(:read) do + authorize_if always() + end + + policy action_type(:create) do + authorize_if actor_present() + end + + policy action(:update) do + authorize_if relates_to_actor_via(:user) + end + + policy action(:destroy) do + authorize_if relates_to_actor_via(:user) + end + + policy action(:like) do + authorize_if actor_present() + end + + policy action(:unlike) do + authorize_if actor_present() + end + end + attributes do uuid_primary_key :id @@ -165,32 +194,6 @@ defmodule Mixer.Posts.Tweet do end end - policies do - policy action_type(:read) do - authorize_if always() - end - - policy action_type(:create) do - authorize_if actor_present() - end - - policy action(:update) do - authorize_if relates_to_actor_via(:user) - end - - policy action(:destroy) do - authorize_if relates_to_actor_via(:user) - end - - policy action(:like) do - authorize_if actor_present() - end - - policy action(:unlike) do - authorize_if actor_present() - end - end - defp ensure_like(_tweet, nil), do: {:error, Ash.Error.Forbidden.exception([])} defp ensure_like(tweet, actor) do diff --git a/lib/mixer/posts/tweet_like.ex b/lib/mixer/posts/tweet_like.ex index 33bde07..a96b79a 100644 --- a/lib/mixer/posts/tweet_like.ex +++ b/lib/mixer/posts/tweet_like.ex @@ -23,6 +23,20 @@ defmodule Mixer.Posts.TweetLike do end end + policies do + policy action_type(:read) do + authorize_if always() + end + + policy action(:create) do + authorize_if actor_present() + end + + policy action_type(:destroy) do + authorize_if relates_to_actor_via(:user) + end + end + attributes do uuid_primary_key :id @@ -52,18 +66,4 @@ defmodule Mixer.Posts.TweetLike do identities do identity :unique_user_tweet, [:tweet_id, :user_id] end - - policies do - policy action_type(:read) do - authorize_if always() - end - - policy action(:create) do - authorize_if actor_present() - end - - policy action_type(:destroy) do - authorize_if relates_to_actor_via(:user) - end - end end diff --git a/lib/mixer_web/controllers/page_controller.ex b/lib/mixer_web/controllers/page_controller.ex index 82a79d0..ac7faee 100644 --- a/lib/mixer_web/controllers/page_controller.ex +++ b/lib/mixer_web/controllers/page_controller.ex @@ -2,7 +2,11 @@ defmodule MixerWeb.PageController do use MixerWeb, :controller def home(conn, _params) do - render(conn, :home) + if conn.assigns[:current_user] do + redirect(conn, to: ~p"/feed") + else + render(conn, :home) + end end def index(conn, _params) do @@ -28,11 +32,11 @@ defmodule MixerWeb.PageController do conn |> put_root_layout(html: {MixerWeb.Layouts, :spa_root}) |> render(:index, - current_user: conn.assigns[:current_user], - media_host: "#{asset_host}/#{bucket}", - page: page, - tweet_id: tweet_id, - user_id: user_id - ) + current_user: conn.assigns[:current_user], + media_host: "#{asset_host}/#{bucket}", + page: page, + tweet_id: tweet_id, + user_id: user_id + ) end end diff --git a/lib/mixer_web/controllers/page_html/index.html.heex b/lib/mixer_web/controllers/page_html/index.html.heex index 295e0ec..96badd5 100644 --- a/lib/mixer_web/controllers/page_html/index.html.heex +++ b/lib/mixer_web/controllers/page_html/index.html.heex @@ -1,8 +1,10 @@ -